Compare commits
1962 Commits
fixed-#145
...
form-row-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f233bc218 | ||
|
|
448f4b94af | ||
|
|
7c58fb93a9 | ||
|
|
80e4e4a40e | ||
|
|
7161d56ae2 | ||
|
|
1e85c7b575 | ||
|
|
03130f0947 | ||
|
|
4812e25e5c | ||
|
|
e6c49da11c | ||
|
|
6f3ee1914a | ||
|
|
9232ee781e | ||
|
|
3d675d375c | ||
|
|
d2edc77197 | ||
|
|
53716cbe90 | ||
|
|
8ce3fe0df0 | ||
|
|
affee0a990 | ||
|
|
6d5f515f48 | ||
|
|
d96844498f | ||
|
|
08609d4f56 | ||
|
|
be344c440f | ||
|
|
96cc2c9ee9 | ||
|
|
fb1a89442c | ||
|
|
359e0197bf | ||
|
|
6dbb836a01 | ||
|
|
3426afe5a8 | ||
|
|
4bbf923eb6 | ||
|
|
e2c3480194 | ||
|
|
73159076f6 | ||
|
|
90d040573d | ||
|
|
155481a442 | ||
|
|
2f31bfc5fe | ||
|
|
8d0c88dc74 | ||
|
|
07256fd833 | ||
|
|
776cd43a58 | ||
|
|
acb5309aab | ||
|
|
c68f81db3c | ||
|
|
ac8e341b37 | ||
|
|
36122b3966 | ||
|
|
79bcf472f0 | ||
|
|
55d86da846 | ||
|
|
e4f8c1ba3f | ||
|
|
c36236b7dc | ||
|
|
63994333d0 | ||
|
|
da4c7d8934 | ||
|
|
186721eca0 | ||
|
|
f53d939c86 | ||
|
|
23e6909708 | ||
|
|
cf421fe1c1 | ||
|
|
4d80e806e4 | ||
|
|
60a7b7f7ff | ||
|
|
90263eab06 | ||
|
|
9d8f251fc4 | ||
|
|
2b4986571c | ||
|
|
890d13bd52 | ||
|
|
e698e71137 | ||
|
|
d064a5530a | ||
|
|
ab4fbf6c19 | ||
|
|
728afa8361 | ||
|
|
b77019c16e | ||
|
|
6703448b80 | ||
|
|
776ba19a1f | ||
|
|
1f499e0d44 | ||
|
|
0a6eb61103 | ||
|
|
32a2eed5ec | ||
|
|
40a70d39d0 | ||
|
|
5697054e98 | ||
|
|
def5969e1c | ||
|
|
78a418630d | ||
|
|
6d76e7b2d4 | ||
|
|
f052c8b44c | ||
|
|
0e6991d56d | ||
|
|
06eab5f8a4 | ||
|
|
4a481e79c4 | ||
|
|
ee4abbcbaa | ||
|
|
dcc82d742f | ||
|
|
19cb2089d7 | ||
|
|
04923b06b0 | ||
|
|
e16755d491 | ||
|
|
742b0769a4 | ||
|
|
df68dca9dc | ||
|
|
4a5bf78d58 | ||
|
|
7947237489 | ||
|
|
1115205164 | ||
|
|
d5d01136c4 | ||
|
|
3d47277614 | ||
|
|
b937bea04f | ||
|
|
fff14632bc | ||
|
|
9bdf1a620f | ||
|
|
60099aa989 | ||
|
|
f3976e5dd8 | ||
|
|
d7d6893304 | ||
|
|
99549ce805 | ||
|
|
7eb15fe04d | ||
|
|
e2019a13ab | ||
|
|
4b7a06761a | ||
|
|
8f4a1f5801 | ||
|
|
891bec9cdb | ||
|
|
c5252ea583 | ||
|
|
82d553c180 | ||
|
|
71e34355b9 | ||
|
|
2bad8c72e4 | ||
|
|
6134ca01ac | ||
|
|
be8193ebff | ||
|
|
430ee46645 | ||
|
|
76fbbf29e8 | ||
|
|
3108159d95 | ||
|
|
f282a1ead7 | ||
|
|
324bc4957d | ||
|
|
c3b5c4dfae | ||
|
|
4ab5d97e86 | ||
|
|
b4696ef11e | ||
|
|
294ffb72a4 | ||
|
|
8c534d29d3 | ||
|
|
d6cb262f9d | ||
|
|
5211e2ae20 | ||
|
|
90832fd1ad | ||
|
|
889cbc69e1 | ||
|
|
15948370d4 | ||
|
|
63c5177b37 | ||
|
|
4fbfaf6b9f | ||
|
|
9e68497b63 | ||
|
|
0b9e13bf1e | ||
|
|
485d343e0f | ||
|
|
07f0e8a3be | ||
|
|
cc5afb1cd8 | ||
|
|
c69f1c0890 | ||
|
|
a8733bdedf | ||
|
|
4222e4eb51 | ||
|
|
5db4441f5c | ||
|
|
2bcfe97211 | ||
|
|
4e74c97c84 | ||
|
|
71a46c9bd6 | ||
|
|
1a7c7fdebf | ||
|
|
65ff3d414a | ||
|
|
9fa38b70c8 | ||
|
|
470172f53f | ||
|
|
81261d9e36 | ||
|
|
3ab2e20119 | ||
|
|
b773d576ea | ||
|
|
23feb64b5a | ||
|
|
87fe9d9d3d | ||
|
|
b1ef3f51cb | ||
|
|
e1b6488f8e | ||
|
|
a472dede2b | ||
|
|
263cc3f7a1 | ||
|
|
598612d4bf | ||
|
|
a07d83e583 | ||
|
|
d355812433 | ||
|
|
1afc14f5ab | ||
|
|
8947b667ae | ||
|
|
6b41796d44 | ||
|
|
6efe8eb55b | ||
|
|
4c8f8918e8 | ||
|
|
4cb5bb1855 | ||
|
|
eb007e025a | ||
|
|
18aefa9dee | ||
|
|
2ec540bd36 | ||
|
|
1374ec408a | ||
|
|
8f8fed2b79 | ||
|
|
a1e6f01fe9 | ||
|
|
d7496f22e5 | ||
|
|
7de25a1c37 | ||
|
|
78da89340c | ||
|
|
60606115fe | ||
|
|
57b49fc31c | ||
|
|
6e90c8f6e6 | ||
|
|
a7ff2595a5 | ||
|
|
1edea6abef | ||
|
|
a3bd58bda6 | ||
|
|
3339d1999f | ||
|
|
df3e8ec0f3 | ||
|
|
2dbec867d9 | ||
|
|
3fc651d659 | ||
|
|
b91d23023d | ||
|
|
cc234c60b8 | ||
|
|
f1883c8004 | ||
|
|
79f6ddc8ee | ||
|
|
89f439a18d | ||
|
|
79eb5bfad9 | ||
|
|
3a36b7dafd | ||
|
|
1fe2fd9891 | ||
|
|
5fdb999ece | ||
|
|
5a38d9c2b6 | ||
|
|
b1c7dc6cbb | ||
|
|
b21b2ac41c | ||
|
|
1396c597ef | ||
|
|
46af72ee4e | ||
|
|
e3e8f553c8 | ||
|
|
549928d3d1 | ||
|
|
abefec9628 | ||
|
|
b483eeded4 | ||
|
|
fab85dafa8 | ||
|
|
955faed919 | ||
|
|
3ff516180d | ||
|
|
0c8ca6d6b0 | ||
|
|
a5de077e04 | ||
|
|
c0a99d6b52 | ||
|
|
a1e65cd897 | ||
|
|
5d65f1ffc5 | ||
|
|
b7193a06fd | ||
|
|
cba090f8eb | ||
|
|
0d6baa1081 | ||
|
|
bc60d796a3 | ||
|
|
85a208526b | ||
|
|
c3ac0a750d | ||
|
|
0f111127a5 | ||
|
|
cd266a6bef | ||
|
|
0f4945621c | ||
|
|
9a477f227b | ||
|
|
3c81257325 | ||
|
|
218fe9ebdc | ||
|
|
24bb45ab97 | ||
|
|
85f39de540 | ||
|
|
5cc3277e2d | ||
|
|
d5ef7f3204 | ||
|
|
cb47d01f51 | ||
|
|
ee499c1385 | ||
|
|
4ae4af5c10 | ||
|
|
b0f5fe7e25 | ||
|
|
fd32585efc | ||
|
|
c675bb7252 | ||
|
|
e65d11d71e | ||
|
|
c2680334f1 | ||
|
|
1217a02ec1 | ||
|
|
143a091f45 | ||
|
|
49ecc93dbe | ||
|
|
1735fb6bed | ||
|
|
8fefc11b4d | ||
|
|
1a3b22171c | ||
|
|
5e773be260 | ||
|
|
c317a1dc8b | ||
|
|
9401ffc83c | ||
|
|
479a852446 | ||
|
|
9188fb03f0 | ||
|
|
36bb91cbc3 | ||
|
|
56e5ab8bc6 | ||
|
|
607eb6ca03 | ||
|
|
ad745cc84b | ||
|
|
5a13d5ea6f | ||
|
|
0b065eb7fe | ||
|
|
e2c0e4bc66 | ||
|
|
655b0e6778 | ||
|
|
cd785c9fc3 | ||
|
|
23885f5166 | ||
|
|
44ef39e419 | ||
|
|
e05a0ef565 | ||
|
|
0ea9c0647f | ||
|
|
00b9ba2b75 | ||
|
|
f1e3bc9531 | ||
|
|
7360e093b2 | ||
|
|
5f639bc24f | ||
|
|
884d2a9552 | ||
|
|
fd8a8b29b1 | ||
|
|
4e1ef40c05 | ||
|
|
00af0ddff5 | ||
|
|
2467e823a5 | ||
|
|
224642fcb5 | ||
|
|
59518ca2c5 | ||
|
|
dfb7c73069 | ||
|
|
70eccceab3 | ||
|
|
3f69b70367 | ||
|
|
94a0a2f8be | ||
|
|
7e23596ab8 | ||
|
|
3c58a5dd3d | ||
|
|
51789ccbf3 | ||
|
|
6ae4a9aa1a | ||
|
|
0aebd669b2 | ||
|
|
2a92c4899d | ||
|
|
530089895a | ||
|
|
ae8289fc8c | ||
|
|
90f4dfb48b | ||
|
|
ec2eddf538 | ||
|
|
44d31d4b39 | ||
|
|
4934e7666c | ||
|
|
35bf0d020e | ||
|
|
4934dc85ac | ||
|
|
5ceb50d7e5 | ||
|
|
ae7ccbb7bd | ||
|
|
1cd9fc47aa | ||
|
|
013c50607a | ||
|
|
d7bf9b7f2e | ||
|
|
4702fdddc6 | ||
|
|
dd06a530c0 | ||
|
|
c36125dc95 | ||
|
|
ae43f93d0a | ||
|
|
8918b17f77 | ||
|
|
dfd05e8b5b | ||
|
|
3daa6dd051 | ||
|
|
14e43192e6 | ||
|
|
6cf88b1792 | ||
|
|
6b9839367f | ||
|
|
34fcf5d616 | ||
|
|
1cf3c74e67 | ||
|
|
16b57b931e | ||
|
|
3457e7d617 | ||
|
|
edbe8001e6 | ||
|
|
71644c1cbe | ||
|
|
03fd8df8bd | ||
|
|
71d622b6dd | ||
|
|
689d5a2d58 | ||
|
|
b2e9eb866c | ||
|
|
c8b7782d1d | ||
|
|
673f936689 | ||
|
|
2ca0d39e51 | ||
|
|
908c8bc397 | ||
|
|
93082e1e87 | ||
|
|
ef0a6aa25e | ||
|
|
b9f4dc1e9d | ||
|
|
90afec864e | ||
|
|
b082fb6692 | ||
|
|
4bbbd786cd | ||
|
|
0c1b2a54e7 | ||
|
|
a6ded20ede | ||
|
|
9b96314371 | ||
|
|
85e16ecd51 | ||
|
|
2ac36cdfd6 | ||
|
|
e40c532354 | ||
|
|
9404dff79c | ||
|
|
55b324d8d6 | ||
|
|
3ed2e2d79e | ||
|
|
3152d9eadd | ||
|
|
f1266ab5d6 | ||
|
|
e70f1408aa | ||
|
|
664e3984e3 | ||
|
|
665c13e238 | ||
|
|
8a667b20c2 | ||
|
|
3693241292 | ||
|
|
a384245368 | ||
|
|
3c3acff79b | ||
|
|
e15de83a95 | ||
|
|
636fccbf97 | ||
|
|
7d8ed399a8 | ||
|
|
272385db6c | ||
|
|
9334b8df47 | ||
|
|
e0bc2ae86f | ||
|
|
2f019bb033 | ||
|
|
291be64aa0 | ||
|
|
75c83236ff | ||
|
|
72be171917 | ||
|
|
4ddee4ac40 | ||
|
|
43cd0d7eb3 | ||
|
|
6b693e2644 | ||
|
|
eeea69d8f2 | ||
|
|
72cf921a4b | ||
|
|
32882f81e7 | ||
|
|
36f5099932 | ||
|
|
bec88a0441 | ||
|
|
6e67e3a8a0 | ||
|
|
4dc3c30354 | ||
|
|
947ccf911d | ||
|
|
06f313febe | ||
|
|
b387136b8f | ||
|
|
31614c5da1 | ||
|
|
146b5a3085 | ||
|
|
397cc1754a | ||
|
|
ff1297cac5 | ||
|
|
8af3cf4056 | ||
|
|
9edec9e212 | ||
|
|
be4362c59a | ||
|
|
8461b147de | ||
|
|
82bdd43168 | ||
|
|
533d82d4d8 | ||
|
|
6f990dd1de | ||
|
|
be848598e3 | ||
|
|
62a58fa23b | ||
|
|
7d0742054f | ||
|
|
dcf7e83507 | ||
|
|
407c2bf0c8 | ||
|
|
c46227ee94 | ||
|
|
d8171eb056 | ||
|
|
c67ca500db | ||
|
|
0081e7b731 | ||
|
|
4e6483d3ed | ||
|
|
0ddf0002c4 | ||
|
|
ad0daf33b9 | ||
|
|
c614c44d4c | ||
|
|
8a46579588 | ||
|
|
fb9fb9c097 | ||
|
|
d9399534ce | ||
|
|
17a749bbed | ||
|
|
0b60c6a939 | ||
|
|
25ce63f00b | ||
|
|
2462bc05b3 | ||
|
|
c3748da0b1 | ||
|
|
90c242a441 | ||
|
|
52239a88b5 | ||
|
|
7a3596c86d | ||
|
|
ac8a9e38f0 | ||
|
|
5c08f3a27e | ||
|
|
2dc11a84bf | ||
|
|
2960ea15f5 | ||
|
|
17aab4c490 | ||
|
|
59d0f0d292 | ||
|
|
27d13a113a | ||
|
|
c58e999fbb | ||
|
|
a02a96d5c4 | ||
|
|
47e9e4704d | ||
|
|
b2ad9d404e | ||
|
|
925d48640d | ||
|
|
5216dd75bf | ||
|
|
028b4e7b79 | ||
|
|
b8b45d2d81 | ||
|
|
4b2b2cb68e | ||
|
|
625a46a2c2 | ||
|
|
be4ace293e | ||
|
|
ebc1e27c22 | ||
|
|
764b363bbc | ||
|
|
705474dc14 | ||
|
|
e639d7726b | ||
|
|
357e85d358 | ||
|
|
9da9166442 | ||
|
|
8ea339f0ef | ||
|
|
e29b0aa6a4 | ||
|
|
d2157868f2 | ||
|
|
89b36ba63f | ||
|
|
1d3dfa1fa4 | ||
|
|
89cfafd933 | ||
|
|
ca567eec8a | ||
|
|
75cfcb83aa | ||
|
|
41da31c379 | ||
|
|
e81f63f46b | ||
|
|
ade03e4827 | ||
|
|
63a4d1ad33 | ||
|
|
33a4c88c3a | ||
|
|
69c5dbfc23 | ||
|
|
cf1bccfd65 | ||
|
|
99acf018f1 | ||
|
|
f04d6f37e5 | ||
|
|
1f79776b8f | ||
|
|
11e5f851f0 | ||
|
|
4ca1db8a1b | ||
|
|
14b829aa30 | ||
|
|
384652b3df | ||
|
|
469069b471 | ||
|
|
9db65c6ae9 | ||
|
|
1346e33e99 | ||
|
|
ab9cc447aa | ||
|
|
cb63c12d2f | ||
|
|
fe9e0444b4 | ||
|
|
6ce0fd20ce | ||
|
|
a18957dbe9 | ||
|
|
13d5b724ee | ||
|
|
b383cd9493 | ||
|
|
c7d8203da9 | ||
|
|
96b5c1d8e1 | ||
|
|
882ee80424 | ||
|
|
e977771fe4 | ||
|
|
4339e4552e | ||
|
|
9bca5912d9 | ||
|
|
b54d222943 | ||
|
|
23756ba1c7 | ||
|
|
e4e613550a | ||
|
|
d1207444db | ||
|
|
06f060161d | ||
|
|
73e0628124 | ||
|
|
7393c4170b | ||
|
|
73e185bf9d | ||
|
|
0bad75b263 | ||
|
|
74b98083e2 | ||
|
|
9034b5ec11 | ||
|
|
77153c3e78 | ||
|
|
927f557672 | ||
|
|
86fb089901 | ||
|
|
630ea05e17 | ||
|
|
7df5196083 | ||
|
|
01f7b5d709 | ||
|
|
07227887f6 | ||
|
|
ec47ee3573 | ||
|
|
13d3b103f1 | ||
|
|
7062962cc8 | ||
|
|
fde447846a | ||
|
|
319cb1bd1e | ||
|
|
58cda5ae6d | ||
|
|
251a3db880 | ||
|
|
30b6dcd767 | ||
|
|
05f6622912 | ||
|
|
36183ac19d | ||
|
|
f6a823e0a8 | ||
|
|
312353551d | ||
|
|
4bdfd0e115 | ||
|
|
fd5c9cee38 | ||
|
|
84bf71802c | ||
|
|
786b20708e | ||
|
|
35739c2eef | ||
|
|
1914a71623 | ||
|
|
dcc53886d9 | ||
|
|
21ef87ef09 | ||
|
|
0e957cad84 | ||
|
|
b67f808da9 | ||
|
|
ad69447b53 | ||
|
|
b4614df88c | ||
|
|
7171247cdc | ||
|
|
6d0084f108 | ||
|
|
29359f42ae | ||
|
|
cf875bf872 | ||
|
|
13c0d335d3 | ||
|
|
ceb33409b5 | ||
|
|
83597d4a8b | ||
|
|
81eefc5448 | ||
|
|
082bc3ece4 | ||
|
|
b2406b61fb | ||
|
|
15698d7694 | ||
|
|
990cd82f97 | ||
|
|
c87829b3e8 | ||
|
|
6799c41d65 | ||
|
|
80c059be58 | ||
|
|
aa3f896538 | ||
|
|
85c728f313 | ||
|
|
d8c17a8a5e | ||
|
|
50e210b2db | ||
|
|
850939367c | ||
|
|
bf4fef9bf7 | ||
|
|
d5175961a4 | ||
|
|
ba3fb8cd66 | ||
|
|
e7e1d6a232 | ||
|
|
712345f3a0 | ||
|
|
54c9bc3dcb | ||
|
|
e796c0da4a | ||
|
|
cf8ff0f43e | ||
|
|
e67ce23a7c | ||
|
|
d9fb7dc754 | ||
|
|
a66bb95a81 | ||
|
|
2249dad9d7 | ||
|
|
c66fa33b2e | ||
|
|
5c4fa630ae | ||
|
|
56eebb9db4 | ||
|
|
ede74ad24e | ||
|
|
d9773f107e | ||
|
|
27542a8f91 | ||
|
|
5dc07b94aa | ||
|
|
e09112f46a | ||
|
|
d7acf721ae | ||
|
|
50a17a82b6 | ||
|
|
eff5232828 | ||
|
|
7eb032d646 | ||
|
|
3eb29b1cdb | ||
|
|
e065f22f8e | ||
|
|
c1b4ba1f85 | ||
|
|
eeaec471f0 | ||
|
|
0d3c8678d8 | ||
|
|
bbddf5f95b | ||
|
|
3b8c8b3af9 | ||
|
|
84753aa13f | ||
|
|
90b84451d8 | ||
|
|
54c8ae41cc | ||
|
|
7d32b1a724 | ||
|
|
69ffd63ca6 | ||
|
|
4857c19eb6 | ||
|
|
d535e23da0 | ||
|
|
30e02544ab | ||
|
|
ee53925bd2 | ||
|
|
c9961f63b4 | ||
|
|
40495b8a17 | ||
|
|
b1de98f05d | ||
|
|
6bc9a82a7a | ||
|
|
09e843a800 | ||
|
|
6504ee37bd | ||
|
|
082bff2fa8 | ||
|
|
ab7bd86336 | ||
|
|
eada0b0bb5 | ||
|
|
f221f9f22a | ||
|
|
6731e44a0d | ||
|
|
acc37045e4 | ||
|
|
a7c5899c16 | ||
|
|
80b02635a9 | ||
|
|
f90de5ec67 | ||
|
|
9a3e046530 | ||
|
|
7f56e461fe | ||
|
|
1da37e0d38 | ||
|
|
0004d4936c | ||
|
|
7a6fdc4e0a | ||
|
|
2eb727bd0c | ||
|
|
57af507170 | ||
|
|
e37f87465c | ||
|
|
324070f345 | ||
|
|
e1aa843b6d | ||
|
|
e652a7fd61 | ||
|
|
2397bfbad0 | ||
|
|
7e2bc8e452 | ||
|
|
00e8fd0483 | ||
|
|
6d8bf2c665 | ||
|
|
77b79dbd95 | ||
|
|
72466f1aab | ||
|
|
dafc6c5136 | ||
|
|
6901deccbf | ||
|
|
5a9c906eb9 | ||
|
|
b95b60b49e | ||
|
|
14408ef18f | ||
|
|
c790147a5c | ||
|
|
6b87c90e02 | ||
|
|
80c39c5ef3 | ||
|
|
2b4d5222eb | ||
|
|
9604ecebad | ||
|
|
9a3e84d84c | ||
|
|
0d67970a45 | ||
|
|
913b9f0c40 | ||
|
|
610a5745f0 | ||
|
|
dff12324c6 | ||
|
|
f340390fc8 | ||
|
|
643960c829 | ||
|
|
be81e74921 | ||
|
|
1737018325 | ||
|
|
2bee8729e4 | ||
|
|
5d03038734 | ||
|
|
75b11de0f4 | ||
|
|
484d5ba76e | ||
|
|
c5bede8594 | ||
|
|
798685d0b8 | ||
|
|
cd9ea6ae3b | ||
|
|
cb7654ae90 | ||
|
|
113b762ec7 | ||
|
|
78704d8b85 | ||
|
|
1109db76fe | ||
|
|
b1b390febf | ||
|
|
be451fa0c0 | ||
|
|
1fa553c785 | ||
|
|
905f61371d | ||
|
|
7da5210a01 | ||
|
|
18172d3896 | ||
|
|
c28e78b9e2 | ||
|
|
e7827a3847 | ||
|
|
039564e74c | ||
|
|
e164595a0f | ||
|
|
d29e09a3ff | ||
|
|
db9f85e9da | ||
|
|
27022954b1 | ||
|
|
30362c924f | ||
|
|
bf63b15b46 | ||
|
|
19aea4bd6c | ||
|
|
090890e9c6 | ||
|
|
00c394345a | ||
|
|
605022a9e3 | ||
|
|
b06c58fe7b | ||
|
|
f5c8b3eb04 | ||
|
|
739980aa09 | ||
|
|
afde5943e3 | ||
|
|
32300cb42c | ||
|
|
dffcb62fa1 | ||
|
|
de3b1697c8 | ||
|
|
8c668b72b7 | ||
|
|
a18fb10b5a | ||
|
|
52140dbe06 | ||
|
|
db5bb1928e | ||
|
|
65b66beb07 | ||
|
|
c83504b4e7 | ||
|
|
cd2e7ee31d | ||
|
|
c3a0a0415a | ||
|
|
709f4672b7 | ||
|
|
b54ecd4da0 | ||
|
|
e6c030b050 | ||
|
|
7bd3a791a1 | ||
|
|
87a7e3501b | ||
|
|
b9cfc03b4f | ||
|
|
daefec3013 | ||
|
|
131327a64d | ||
|
|
183a9742c4 | ||
|
|
77d002a158 | ||
|
|
94699893ac | ||
|
|
9f81989bdd | ||
|
|
15abe36c53 | ||
|
|
3094e007ee | ||
|
|
eb259aee22 | ||
|
|
04b83f8176 | ||
|
|
c05c8defb9 | ||
|
|
bf5668a42e | ||
|
|
335ab3f064 | ||
|
|
ec310bc8fb | ||
|
|
db477421b2 | ||
|
|
30a9496cf5 | ||
|
|
6cefa0d0b3 | ||
|
|
9284984265 | ||
|
|
53b96168a9 | ||
|
|
eadce51f10 | ||
|
|
7dd493da35 | ||
|
|
b3c583b6dc | ||
|
|
560bd6da92 | ||
|
|
28abeab31d | ||
|
|
a5824ccc5f | ||
|
|
830a7964a4 | ||
|
|
12a649ec4b | ||
|
|
35b79e4d14 | ||
|
|
751dad7f2e | ||
|
|
b08d86220a | ||
|
|
3a27ecc475 | ||
|
|
da6fab5d43 | ||
|
|
ca95b29cd6 | ||
|
|
c5c68e9dd5 | ||
|
|
44fbde26fa | ||
|
|
6e2bcd6aa9 | ||
|
|
b1359c3277 | ||
|
|
9c0202e5ce | ||
|
|
39ef353073 | ||
|
|
7b5d90dd81 | ||
|
|
0ba8f5cc5a | ||
|
|
d1129081df | ||
|
|
315a812df5 | ||
|
|
6fb9e2c38e | ||
|
|
cfc979acf0 | ||
|
|
d7407d70a3 | ||
|
|
8ccd2e97a8 | ||
|
|
988204619f | ||
|
|
cad6cc3007 | ||
|
|
eebc2ab8be | ||
|
|
b303875f1d | ||
|
|
d5cc61f378 | ||
|
|
0d7ec43262 | ||
|
|
d3747f4daa | ||
|
|
af695e7dc8 | ||
|
|
1edbfd87df | ||
|
|
454be01a6c | ||
|
|
b65b3151ee | ||
|
|
745fc515f1 | ||
|
|
715b9c1182 | ||
|
|
1d24b7985b | ||
|
|
526bb2c650 | ||
|
|
c450c0ddb8 | ||
|
|
95be847d87 | ||
|
|
c1a6546eba | ||
|
|
648c25a0a7 | ||
|
|
f2ec7f2975 | ||
|
|
f518af6d61 | ||
|
|
13a0f49f5f | ||
|
|
b11c6a5c06 | ||
|
|
5822e4e692 | ||
|
|
e4f06b0ca8 | ||
|
|
2f093c0e82 | ||
|
|
5d9dc0e74d | ||
|
|
199eefafa1 | ||
|
|
adc3a34929 | ||
|
|
cb2ffe6b3f | ||
|
|
c5b58f9ecc | ||
|
|
b3e3d01672 | ||
|
|
6b68fe4de6 | ||
|
|
4a6520fc78 | ||
|
|
75ab6c9b13 | ||
|
|
2f77fcb526 | ||
|
|
3461bbfdb3 | ||
|
|
60604c3481 | ||
|
|
51f6927076 | ||
|
|
671c113cd2 | ||
|
|
8a74d21ede | ||
|
|
75995b2109 | ||
|
|
4298aad008 | ||
|
|
823c67400d | ||
|
|
3160d1064d | ||
|
|
d1eefc3fea | ||
|
|
16795382fc | ||
|
|
eb17974adc | ||
|
|
22852c27f8 | ||
|
|
f4a94d975d | ||
|
|
7a36bbbd1e | ||
|
|
2b401b965b | ||
|
|
314bc5b44f | ||
|
|
76374f0d5a | ||
|
|
264efb015e | ||
|
|
e74460aefc | ||
|
|
55a5a12b30 | ||
|
|
58944a38eb | ||
|
|
469e3bd475 | ||
|
|
17650c5735 | ||
|
|
15e64155b5 | ||
|
|
39955ac760 | ||
|
|
855a176ca9 | ||
|
|
47b2b30455 | ||
|
|
b702e3e2de | ||
|
|
a6b74d56c6 | ||
|
|
a4222bcaef | ||
|
|
ecf24511cd | ||
|
|
abb097a391 | ||
|
|
1d88cf443f | ||
|
|
7b6c0c3a40 | ||
|
|
fdb0651bf4 | ||
|
|
dd742a2e4a | ||
|
|
c39d484611 | ||
|
|
c42996429f | ||
|
|
a091baf5a6 | ||
|
|
128bdf500a | ||
|
|
73ac00bc51 | ||
|
|
3524e23e38 | ||
|
|
be0f3910bb | ||
|
|
07dbc6842c | ||
|
|
5a16b59462 | ||
|
|
13cd7071b8 | ||
|
|
40108b196c | ||
|
|
c8e79aa5ca | ||
|
|
e60f2b2332 | ||
|
|
b6d397bcca | ||
|
|
918426a2fa | ||
|
|
c076b37c9b | ||
|
|
7c58bfa282 | ||
|
|
0caaba156d | ||
|
|
c22575812d | ||
|
|
0ede4da816 | ||
|
|
98e23ff92e | ||
|
|
6503f9c667 | ||
|
|
4770e469b4 | ||
|
|
29a18c7c8b | ||
|
|
6db0003e3f | ||
|
|
c538c460fa | ||
|
|
822339fe42 | ||
|
|
b84d9282ca | ||
|
|
00a17cd55e | ||
|
|
952b6f33bb | ||
|
|
c57c4b8ff2 | ||
|
|
39e6223ff2 | ||
|
|
d8dd274c08 | ||
|
|
15f97b6cb9 | ||
|
|
fc091c1174 | ||
|
|
c07ef4d87f | ||
|
|
643d44af22 | ||
|
|
b934f43db0 | ||
|
|
c7bdad649a | ||
|
|
18c2508d2f | ||
|
|
4dcfd8b353 | ||
|
|
726116574d | ||
|
|
27f02014ca | ||
|
|
f80f1acaa7 | ||
|
|
39d5ffeceb | ||
|
|
48ba7eed3e | ||
|
|
f39afe5a65 | ||
|
|
11eee833bb | ||
|
|
35b358d336 | ||
|
|
ae109be631 | ||
|
|
3f7ed73395 | ||
|
|
7612ee6b08 | ||
|
|
fec9d716ee | ||
|
|
da5b1afd19 | ||
|
|
618106c103 | ||
|
|
2ed2b0101a | ||
|
|
312be98132 | ||
|
|
e0bb77a6d6 | ||
|
|
5ca9d31964 | ||
|
|
855922c21a | ||
|
|
bc645d2621 | ||
|
|
2fcd8cd261 | ||
|
|
9c06ff3899 | ||
|
|
2a37aa3b49 | ||
|
|
0ffa47a2c6 | ||
|
|
bf591320af | ||
|
|
56e687bed2 | ||
|
|
9caa240fdb | ||
|
|
3ffb73a516 | ||
|
|
cc1132be87 | ||
|
|
1c31f126ef | ||
|
|
d8eaf2676f | ||
|
|
07b25fe376 | ||
|
|
c2ecd20b7d | ||
|
|
1b42abcc98 | ||
|
|
9efb49d510 | ||
|
|
2d6270c697 | ||
|
|
0823c23a6e | ||
|
|
b3f0ce4b2a | ||
|
|
8b83584b67 | ||
|
|
9eb686fe08 | ||
|
|
765051ce88 | ||
|
|
ed402e0122 | ||
|
|
e203d4dee3 | ||
|
|
1488271a83 | ||
|
|
b47d773e13 | ||
|
|
48bbf8d005 | ||
|
|
e97b969d66 | ||
|
|
a8d0a4a95d | ||
|
|
cdd12df81a | ||
|
|
3fb0804cef | ||
|
|
050a3afc74 | ||
|
|
270401c693 | ||
|
|
551822ce7d | ||
|
|
4b8c371097 | ||
|
|
90fbf6da46 | ||
|
|
0c3103e3d2 | ||
|
|
6a8e1566fe | ||
|
|
ced30082a6 | ||
|
|
f6c64abc1a | ||
|
|
6811ebcd52 | ||
|
|
7f9939a896 | ||
|
|
1c99f2dfdd | ||
|
|
1974fccac3 | ||
|
|
911552035e | ||
|
|
ff25d275ee | ||
|
|
1fcf5e03e7 | ||
|
|
4fe7bfb851 | ||
|
|
9b4101855f | ||
|
|
9253d894d3 | ||
|
|
fb60985d03 | ||
|
|
ebd79f22c7 | ||
|
|
c1b139fb9a | ||
|
|
8f575923cf | ||
|
|
a88bcea8ca | ||
|
|
21566560a7 | ||
|
|
0ecfd02649 | ||
|
|
e3ca43bf40 | ||
|
|
420aaf4f61 | ||
|
|
61abb8d5cb | ||
|
|
0c35f213e1 | ||
|
|
ecad656551 | ||
|
|
615e6d6e4f | ||
|
|
f68813af13 | ||
|
|
6dceefb96e | ||
|
|
69eff394fd | ||
|
|
a9da3aca81 | ||
|
|
91f3556375 | ||
|
|
aab7c3a840 | ||
|
|
9c823119e3 | ||
|
|
f5128833f6 | ||
|
|
2bc144354a | ||
|
|
e6fec6ec34 | ||
|
|
37a90d0ce9 | ||
|
|
53389875bf | ||
|
|
3b243b38c8 | ||
|
|
02f1291e8f | ||
|
|
3d9580808b | ||
|
|
2141ee71d4 | ||
|
|
01dd07083e | ||
|
|
42a28ea06b | ||
|
|
92e4f6b5d9 | ||
|
|
180cb6ba8e | ||
|
|
a78762e40b | ||
|
|
7b7738fbcc | ||
|
|
9797bb19e2 | ||
|
|
3101212c49 | ||
|
|
3f0ac103a1 | ||
|
|
59bd6ca360 | ||
|
|
22fe9a786e | ||
|
|
08a9554b3c | ||
|
|
d79bd825ee | ||
|
|
fe3d225cfa | ||
|
|
31197604a3 | ||
|
|
d2ee8de9ac | ||
|
|
03d3fb6a5f | ||
|
|
376e0db66e | ||
|
|
5fdabc1a62 | ||
|
|
dfe2a75d72 | ||
|
|
ba85af11aa | ||
|
|
db58b80d27 | ||
|
|
5cb8aae383 | ||
|
|
817530429b | ||
|
|
e33b1b6c90 | ||
|
|
30520297e8 | ||
|
|
78ca1d1335 | ||
|
|
6159ee8c2c | ||
|
|
5cd5392958 | ||
|
|
0dcdfc5d14 | ||
|
|
d0e068f1c0 | ||
|
|
4a7b7183d2 | ||
|
|
f42a2d7457 | ||
|
|
94bd39cf23 | ||
|
|
4038a22093 | ||
|
|
d29619b67c | ||
|
|
682baec0c9 | ||
|
|
ff91be491d | ||
|
|
ef35a0f2f1 | ||
|
|
f5235cb835 | ||
|
|
f12a3bb08b | ||
|
|
ee830e0cb4 | ||
|
|
c8a5065ffa | ||
|
|
23da5573f3 | ||
|
|
b08f985776 | ||
|
|
9b968baaa7 | ||
|
|
07edbe6f1c | ||
|
|
0cd3be003d | ||
|
|
1f55a8b6e3 | ||
|
|
f6b9e11810 | ||
|
|
c93e35ec77 | ||
|
|
c18a3e4266 | ||
|
|
9538a76232 | ||
|
|
5840ef1c6f | ||
|
|
7974baddf5 | ||
|
|
05876bb124 | ||
|
|
4bf569758f | ||
|
|
8bcd5a6d2a | ||
|
|
f56fd9bb0b | ||
|
|
a36afbcb25 | ||
|
|
357ee5fc45 | ||
|
|
ebd8d085cf | ||
|
|
c6dea085b2 | ||
|
|
505148b024 | ||
|
|
8782c3ecec | ||
|
|
b636cf2ef0 | ||
|
|
6dee2b8601 | ||
|
|
bcf301ac17 | ||
|
|
bf2120fb31 | ||
|
|
de56b74f3e | ||
|
|
2f146abe91 | ||
|
|
543d41b6ff | ||
|
|
8da0dd7563 | ||
|
|
a2217d7dbc | ||
|
|
ea84728a3f | ||
|
|
b2d10f7ccf | ||
|
|
b6af25ce99 | ||
|
|
7a9d2454d4 | ||
|
|
a9254cff02 | ||
|
|
d14b34141c | ||
|
|
14bc2cc1ba | ||
|
|
a91b54b97a | ||
|
|
ead655e1db | ||
|
|
c5f28748f7 | ||
|
|
ee4831cb30 | ||
|
|
deb1afd28b | ||
|
|
9e8eead71e | ||
|
|
3f96f7cbd7 | ||
|
|
dde2e88332 | ||
|
|
ff25015595 | ||
|
|
7d0c695808 | ||
|
|
906385def9 | ||
|
|
a6c6c7eae9 | ||
|
|
205725c767 | ||
|
|
c207efbb35 | ||
|
|
c0211e59b3 | ||
|
|
dd2678cbb9 | ||
|
|
e2c87b664e | ||
|
|
29d4b4dd53 | ||
|
|
3fba307e55 | ||
|
|
7171fa36d8 | ||
|
|
c570f656bf | ||
|
|
a5e37519f5 | ||
|
|
0f88d6eec3 | ||
|
|
651c51bb01 | ||
|
|
3eefeec4ce | ||
|
|
b61419c1ce | ||
|
|
f590fcffbc | ||
|
|
0fdbdfd5c2 | ||
|
|
31056ff858 | ||
|
|
8d2643696b | ||
|
|
e7488d19e9 | ||
|
|
2bb3b6d64c | ||
|
|
5744e48ae8 | ||
|
|
82d0a21440 | ||
|
|
58133cffac | ||
|
|
bfd8c2f310 | ||
|
|
30d447c023 | ||
|
|
9a0846b8a6 | ||
|
|
e87e924ac2 | ||
|
|
3667fcddd7 | ||
|
|
90f261bab6 | ||
|
|
906741d662 | ||
|
|
f7dfb09a4d | ||
|
|
12be088c4f | ||
|
|
6737ba80cd | ||
|
|
862a3d938e | ||
|
|
09e82377a5 | ||
|
|
59470864e7 | ||
|
|
c95aeb3730 | ||
|
|
5c55c90d68 | ||
|
|
e47972731b | ||
|
|
5851cc9e41 | ||
|
|
6f615230e9 | ||
|
|
d91598a25e | ||
|
|
9e416778d9 | ||
|
|
860a117567 | ||
|
|
b8fe3b18d4 | ||
|
|
40269a724b | ||
|
|
ec828318d8 | ||
|
|
d31e7ed534 | ||
|
|
5c2dbe438b | ||
|
|
10857635ac | ||
|
|
df2545ef80 | ||
|
|
f6ff729316 | ||
|
|
38678803eb | ||
|
|
67c931f196 | ||
|
|
3135917127 | ||
|
|
1c23092d0e | ||
|
|
a90ff21cbf | ||
|
|
0ce0cee81f | ||
|
|
f4be5ffb5d | ||
|
|
19958748bf | ||
|
|
d6ca8468e3 | ||
|
|
7bccb7718b | ||
|
|
f6b63b5e44 | ||
|
|
9a2c5ff195 | ||
|
|
3597f759da | ||
|
|
3ed3b21286 | ||
|
|
b89b636474 | ||
|
|
2afc595452 | ||
|
|
52afa3d36d | ||
|
|
c7262f2885 | ||
|
|
8662aa2277 | ||
|
|
242aa60e04 | ||
|
|
8095e0ab72 | ||
|
|
7a3c2c27ff | ||
|
|
be3c8ddd5c | ||
|
|
ec5b9ce903 | ||
|
|
bd2acefecc | ||
|
|
18e49e9067 | ||
|
|
5d124360c2 | ||
|
|
a0d65520a3 | ||
|
|
365d7448d5 | ||
|
|
a35731d9d5 | ||
|
|
9a0102c723 | ||
|
|
9d3623cca6 | ||
|
|
2fe08a721f | ||
|
|
7abc3a7d7d | ||
|
|
d4a34f1a3c | ||
|
|
2f77f2cb2b | ||
|
|
ddda4848d3 | ||
|
|
528e3a2106 | ||
|
|
8516856d37 | ||
|
|
032a664d4c | ||
|
|
132327594b | ||
|
|
d2a2c63070 | ||
|
|
aac1864c9b | ||
|
|
170a5158fa | ||
|
|
1d8493d388 | ||
|
|
ff39e8bd2c | ||
|
|
c3442033da | ||
|
|
f1dd84edba | ||
|
|
e3477f3306 | ||
|
|
06b040a337 | ||
|
|
6620a4f87b | ||
|
|
fa546ddc5b | ||
|
|
f811352c79 | ||
|
|
7ed8963b9f | ||
|
|
c0e9dff5bf | ||
|
|
a9fc8b79fd | ||
|
|
afd794b4c7 | ||
|
|
c4a28f0ec4 | ||
|
|
db343bf795 | ||
|
|
0157043dc5 | ||
|
|
a947f9bd32 | ||
|
|
2a4181c7c3 | ||
|
|
30192f5b14 | ||
|
|
c41b5e8844 | ||
|
|
b27928807b | ||
|
|
16f1b5e23e | ||
|
|
ed651b6869 | ||
|
|
b9d925c7aa | ||
|
|
3650a29381 | ||
|
|
de84ee3693 | ||
|
|
42ba31591d | ||
|
|
a78a243e20 | ||
|
|
38924ced4a | ||
|
|
5e8cc66f5c | ||
|
|
1353837584 | ||
|
|
7cb5a89523 | ||
|
|
1db09a7953 | ||
|
|
bc6aa12dd0 | ||
|
|
c3bea88979 | ||
|
|
6e85e466b0 | ||
|
|
3327cc70c9 | ||
|
|
c9eac66a93 | ||
|
|
53e9bd6e48 | ||
|
|
eaa18e1efb | ||
|
|
afa3dacc31 | ||
|
|
c803c4a57a | ||
|
|
2d3a53e449 | ||
|
|
2d961c435a | ||
|
|
5e076754ce | ||
|
|
927e217961 | ||
|
|
80b48101aa | ||
|
|
08530e6133 | ||
|
|
97130ef6c1 | ||
|
|
da37feae6d | ||
|
|
f96172e61f | ||
|
|
e35477b8db | ||
|
|
cea5560a67 | ||
|
|
311bd5e67e | ||
|
|
1cfddf2a4c | ||
|
|
abe58117fe | ||
|
|
ee5f89f70d | ||
|
|
4f545ed101 | ||
|
|
136de4208e | ||
|
|
7650a2c2a7 | ||
|
|
c3d1987fac | ||
|
|
12ef78bb1c | ||
|
|
16c4241a6e | ||
|
|
4992c77818 | ||
|
|
3a0b1de136 | ||
|
|
1c3ef02c7b | ||
|
|
f268fe9e80 | ||
|
|
2ed98c17d4 | ||
|
|
571ae4fbfd | ||
|
|
c94a8c42f4 | ||
|
|
16fdb16a56 | ||
|
|
822f9a6f28 | ||
|
|
b264bbf69f | ||
|
|
6e61e94e02 | ||
|
|
6a7972c5a1 | ||
|
|
db4fbe315a | ||
|
|
f3613d7103 | ||
|
|
cbbed36428 | ||
|
|
e86e9697b3 | ||
|
|
fd6b2d5715 | ||
|
|
fbb36d1665 | ||
|
|
07be1b8192 | ||
|
|
33880393ac | ||
|
|
5123fe7838 | ||
|
|
cbe26a365d | ||
|
|
f1bb72b2a6 | ||
|
|
2c33654395 | ||
|
|
7c95f03166 | ||
|
|
dd86de017e | ||
|
|
3eabde9630 | ||
|
|
640c51af31 | ||
|
|
7167b17d25 | ||
|
|
8a35948678 | ||
|
|
0fe63d3fb9 | ||
|
|
e4302c3e88 | ||
|
|
a7df6fb465 | ||
|
|
133e7598e0 | ||
|
|
c1a52ffa75 | ||
|
|
4f46313388 | ||
|
|
03b2cc9cd2 | ||
|
|
1a2bf8dc95 | ||
|
|
dd63fbeb84 | ||
|
|
59e435c418 | ||
|
|
f89f0a19b5 | ||
|
|
cbc6ef95cb | ||
|
|
0ceecc9e1d | ||
|
|
c816902025 | ||
|
|
cfb03cdca0 | ||
|
|
266f77b08c | ||
|
|
31e5c13b50 | ||
|
|
257d58c236 | ||
|
|
4a9fe4f981 | ||
|
|
015f3d936c | ||
|
|
18d2a0ffd7 | ||
|
|
4fcc5587ee | ||
|
|
24afde0e46 | ||
|
|
6ca49a20ce | ||
|
|
8499faa55a | ||
|
|
28f293fdc1 | ||
|
|
c60dd809b8 | ||
|
|
297b8e33f2 | ||
|
|
d0593c6b8d | ||
|
|
8a40d7e35c | ||
|
|
b670b2014c | ||
|
|
440e969f52 | ||
|
|
14b79f2f1c | ||
|
|
b3e7619adc | ||
|
|
00cf49a61f | ||
|
|
4f534e0e84 | ||
|
|
6e56d56137 | ||
|
|
83a19fbbbf | ||
|
|
610cb884fc | ||
|
|
2528f6a07b | ||
|
|
ba92cec62b | ||
|
|
d92e961a52 | ||
|
|
b13e74756a | ||
|
|
4ef3072766 | ||
|
|
423d07c919 | ||
|
|
e96e2461d3 | ||
|
|
7a2e2be169 | ||
|
|
cc608de4bf | ||
|
|
8d2a5a7e4a | ||
|
|
b7b0e4fab5 | ||
|
|
a624a79b30 | ||
|
|
313135da6f | ||
|
|
58d27d1247 | ||
|
|
f999a68608 | ||
|
|
edfb28168f | ||
|
|
8d0e03bb06 | ||
|
|
855f6f77cf | ||
|
|
6236cffe14 | ||
|
|
322a71fbb8 | ||
|
|
4d9f8476f3 | ||
|
|
d7d93b14b2 | ||
|
|
db78a9f18f | ||
|
|
d1af3ece6e | ||
|
|
816039f48e | ||
|
|
8153b20984 | ||
|
|
ae240bae6d | ||
|
|
9e30c69e6d | ||
|
|
a50f605c29 | ||
|
|
daf23edd10 | ||
|
|
43c7de9049 | ||
|
|
7e51c5db81 | ||
|
|
2eaaeb8259 | ||
|
|
a02c62d62c | ||
|
|
e0232a8e84 | ||
|
|
6ea5693b2f | ||
|
|
0ee3c45e7b | ||
|
|
030c2114d1 | ||
|
|
2cb18e3668 | ||
|
|
cd9f8be563 | ||
|
|
a02792e9bf | ||
|
|
41bb422244 | ||
|
|
54663d3342 | ||
|
|
2529f7369f | ||
|
|
909c33dccf | ||
|
|
1adc9f1aa9 | ||
|
|
e9948f0718 | ||
|
|
49da9e58fd | ||
|
|
2f74a8afe1 | ||
|
|
f3e288d078 | ||
|
|
988000952e | ||
|
|
981e69929c | ||
|
|
6537f3794b | ||
|
|
d31718ba8a | ||
|
|
9dd4bc5fa8 | ||
|
|
df5f1bd522 | ||
|
|
ddffab9169 | ||
|
|
553ab8851a | ||
|
|
1eae5d12fc | ||
|
|
0c34073582 | ||
|
|
14674947cb | ||
|
|
51bccdbd66 | ||
|
|
f0fbb3cf36 | ||
|
|
0cc47aacbe | ||
|
|
fafd592290 | ||
|
|
40e754b8c3 | ||
|
|
483301db7a | ||
|
|
218606fbd6 | ||
|
|
c601b8e62c | ||
|
|
2bd68ec991 | ||
|
|
66842648ed | ||
|
|
ce54b9a7b5 | ||
|
|
8a5f6d2a5d | ||
|
|
1d86a5476f | ||
|
|
ca4d3f6bce | ||
|
|
2812f2ce92 | ||
|
|
5c623db798 | ||
|
|
edaf005fe1 | ||
|
|
4f6e407247 | ||
|
|
e30881239c | ||
|
|
bbde2cc4b2 | ||
|
|
16d18c79d7 | ||
|
|
a0d2cb8a03 | ||
|
|
1bb5dc7e69 | ||
|
|
58759acfe4 | ||
|
|
0cd5136052 | ||
|
|
b3c6fe5369 | ||
|
|
599718f84e | ||
|
|
d9a5452388 | ||
|
|
0fe49e04bf | ||
|
|
a98d3fb4dc | ||
|
|
8c670d1832 | ||
|
|
c232f490bc | ||
|
|
c7280953dd | ||
|
|
8f4c606c64 | ||
|
|
6740afab42 | ||
|
|
5df22b3e6a | ||
|
|
3d9d18a0d5 | ||
|
|
0102599708 | ||
|
|
960edd4adf | ||
|
|
3547fa723c | ||
|
|
7a456185c6 | ||
|
|
dd79c3f2d6 | ||
|
|
35682d11f0 | ||
|
|
d04b3f0907 | ||
|
|
c926358e04 | ||
|
|
856ba52f36 | ||
|
|
a5bea31154 | ||
|
|
2afcc1e384 | ||
|
|
fc469707a3 | ||
|
|
8863208333 | ||
|
|
77fdc370c7 | ||
|
|
5f38a74a72 | ||
|
|
301290fb6d | ||
|
|
fe15dacb1f | ||
|
|
07fffe2f79 | ||
|
|
0227a63fa5 | ||
|
|
27764b863c | ||
|
|
032fd75f9e | ||
|
|
0bf4f861f3 | ||
|
|
fd8f90cb52 | ||
|
|
b6c6b025c8 | ||
|
|
3d89e98d1f | ||
|
|
7c5110ed5d | ||
|
|
0a474f48ad | ||
|
|
c409bfd5be | ||
|
|
39d5d5b2e0 | ||
|
|
c2d44cf2f2 | ||
|
|
8a80d9009d | ||
|
|
f62b5df566 | ||
|
|
826521f053 | ||
|
|
f9b05bc8de | ||
|
|
b8239e8ed9 | ||
|
|
214757ab0b | ||
|
|
f130186b37 | ||
|
|
2244eebc3b | ||
|
|
4176792f2d | ||
|
|
1e6cef52c9 | ||
|
|
a0f4f30a50 | ||
|
|
4cbf6ac393 | ||
|
|
af7425d8e6 | ||
|
|
3fea909d3f | ||
|
|
7c37d40677 | ||
|
|
3a97c27350 | ||
|
|
e0516a52a8 | ||
|
|
a85ec6efb3 | ||
|
|
3795c74814 | ||
|
|
27954dc6d3 | ||
|
|
68c4187a09 | ||
|
|
b9834231f3 | ||
|
|
2be343ea1c | ||
|
|
109fe1b62c | ||
|
|
63d691a63c | ||
|
|
7f1bdb6f34 | ||
|
|
6f57d6b876 | ||
|
|
e0bad99ea1 | ||
|
|
7cdfaa93ec | ||
|
|
59ccc70303 | ||
|
|
f1584b722d | ||
|
|
e39eb09cfb | ||
|
|
64d397c3f3 | ||
|
|
465ac1d1e1 | ||
|
|
18d6becebc | ||
|
|
943a4093ad | ||
|
|
b0305e12d2 | ||
|
|
b0917a5131 | ||
|
|
0972c4e340 | ||
|
|
4d8c5a86a4 | ||
|
|
3bbd0fdbcd | ||
|
|
43a237bf95 | ||
|
|
95f867b267 | ||
|
|
58f76b5c99 | ||
|
|
e96daf469a | ||
|
|
7c4ee632cf | ||
|
|
f2cdfe9e47 | ||
|
|
b6b0f716eb | ||
|
|
929b67e768 | ||
|
|
0573dc136a | ||
|
|
48588f6a9e | ||
|
|
8214b11da5 | ||
|
|
36090bf83e | ||
|
|
bffb2fe82f | ||
|
|
500cbf5d92 | ||
|
|
bd0e04ed15 | ||
|
|
88579b9bf3 | ||
|
|
8599981d44 | ||
|
|
e8bb9bde99 | ||
|
|
0ee3cca4da | ||
|
|
6fc6e95c67 | ||
|
|
f89ee6b7f2 | ||
|
|
43b585bde8 | ||
|
|
aebfb52c85 | ||
|
|
667bd7af0e | ||
|
|
710f89291f | ||
|
|
bbf69bc582 | ||
|
|
f2b7a3d002 | ||
|
|
3fd9e3ab56 | ||
|
|
5f835aa009 | ||
|
|
d5ca543719 | ||
|
|
4c6249eb9e | ||
|
|
8e11466a54 | ||
|
|
016900bad8 | ||
|
|
dade9797d5 | ||
|
|
2e8ae33761 | ||
|
|
97c1e65ffc | ||
|
|
b4e22f4a21 | ||
|
|
2b8ea9a233 | ||
|
|
0d325060da | ||
|
|
58b6feb3ca | ||
|
|
41c4920d45 | ||
|
|
1a6e98e18f | ||
|
|
d1ddd8de98 | ||
|
|
97e34595f6 | ||
|
|
f41307eb4a | ||
|
|
59de77feb0 | ||
|
|
8ebbcf6e80 | ||
|
|
24c6e836dd | ||
|
|
8e38b3898e | ||
|
|
a179d5234b | ||
|
|
ce9a5e35c9 | ||
|
|
b092779697 | ||
|
|
64c6121fdb | ||
|
|
ab30a96d16 | ||
|
|
08d8954a85 | ||
|
|
dab0fb16ad | ||
|
|
4f20955d0d | ||
|
|
5be398bc99 | ||
|
|
3a703c8bcf | ||
|
|
fe4172957f | ||
|
|
ff3a59d347 | ||
|
|
f9aedea26f | ||
|
|
5abd2c7151 | ||
|
|
bfcaf4f37b | ||
|
|
5f4e1835bc | ||
|
|
c1f1ae6b64 | ||
|
|
c4fcc6c24e | ||
|
|
ccbffa086b | ||
|
|
dd73ad9941 | ||
|
|
ac21f7569f | ||
|
|
4ef0158da4 | ||
|
|
4db3b3ba0e | ||
|
|
dc43d85323 | ||
|
|
62651f381c | ||
|
|
3e9098907a | ||
|
|
e18df250f8 | ||
|
|
be5c5a51da | ||
|
|
a728fad675 | ||
|
|
07ee4be840 | ||
|
|
185629b310 | ||
|
|
30ebea4f2d | ||
|
|
b135c1eac2 | ||
|
|
88fef73d6f | ||
|
|
556a9039e9 | ||
|
|
cdfe6c21c1 | ||
|
|
b094ebdd66 | ||
|
|
4cc9b2d312 | ||
|
|
526a7ddea6 | ||
|
|
bb5ad31cba | ||
|
|
549da2efed | ||
|
|
e5e586dc43 | ||
|
|
8a682beb0e | ||
|
|
24dddae1d1 | ||
|
|
699e9f75c9 | ||
|
|
ad0165d085 | ||
|
|
759e30977b | ||
|
|
6cfdb49cc3 | ||
|
|
39dc38c5d1 | ||
|
|
1195121bf0 | ||
|
|
8bc067b18b | ||
|
|
76f59f7b85 | ||
|
|
55ebb4671f | ||
|
|
8a9cf07063 | ||
|
|
ca9ff8cf19 | ||
|
|
7217d9c427 | ||
|
|
9d712ad8f1 | ||
|
|
f3e49e7010 | ||
|
|
046ce19dbb | ||
|
|
ba94f1b920 | ||
|
|
edcd46dd67 | ||
|
|
5cf6c89dde | ||
|
|
58676b1f83 | ||
|
|
8ff7c30e5a | ||
|
|
cd989768d4 | ||
|
|
33263f5a93 | ||
|
|
6cbdefe3d9 | ||
|
|
8ef8e76300 | ||
|
|
c6ecc0d8e8 | ||
|
|
e0f5663bf4 | ||
|
|
aafc8996c1 | ||
|
|
128da40cbf | ||
|
|
ea0460e97e | ||
|
|
d8e7123576 | ||
|
|
73cfdae9e7 | ||
|
|
6f45ec655f | ||
|
|
54a3e41281 | ||
|
|
ec17c168ea | ||
|
|
119b097521 | ||
|
|
c731633a84 | ||
|
|
6a4d6ade39 | ||
|
|
b3b4697fc9 | ||
|
|
72c706d697 | ||
|
|
8a7af24bd4 | ||
|
|
dd01bd3e5f | ||
|
|
59cade9f82 | ||
|
|
6bb9b79832 | ||
|
|
f8fe7b5803 | ||
|
|
4d6279d61c | ||
|
|
5ef581f328 | ||
|
|
d59ba6da84 | ||
|
|
20a59c343e | ||
|
|
d6feb522b7 | ||
|
|
951aee8292 | ||
|
|
6e2d7912b5 | ||
|
|
1b28b06934 | ||
|
|
b20925b550 | ||
|
|
c8b213c190 | ||
|
|
942de9dce5 | ||
|
|
483f684b04 | ||
|
|
26774b4193 | ||
|
|
a7a597d609 | ||
|
|
1c12b9278a | ||
|
|
de4764bd05 | ||
|
|
ff3e69a56c | ||
|
|
c1e7a78d23 | ||
|
|
8894bb91cc | ||
|
|
c955126f01 | ||
|
|
ce53b48d04 | ||
|
|
6b975a5fb4 | ||
|
|
6015aeddee | ||
|
|
7b04e30964 | ||
|
|
6794f5e783 | ||
|
|
6e41ceff39 | ||
|
|
7ab47ff0de | ||
|
|
3a02b15124 | ||
|
|
92d24d8702 | ||
|
|
bcbfd46682 | ||
|
|
bfd96a695f | ||
|
|
f27e8534dc | ||
|
|
040cd7ddbf | ||
|
|
5f73d81935 | ||
|
|
8d6b21a076 | ||
|
|
2d36b25017 | ||
|
|
002b5c0f6f | ||
|
|
1f9e4306ae | ||
|
|
8086842570 | ||
|
|
5242e0b36e | ||
|
|
e50505532e | ||
|
|
51f2d5a664 | ||
|
|
f05ef18d55 | ||
|
|
f6eccd7277 | ||
|
|
4d1258c64b | ||
|
|
103cbfd038 | ||
|
|
47069ad3f4 | ||
|
|
317f620992 | ||
|
|
8f43694582 | ||
|
|
df30076ffd | ||
|
|
f81750617e | ||
|
|
e4534c4319 | ||
|
|
d6c09aae6b | ||
|
|
d3d5230d0c | ||
|
|
db63ad1cf4 | ||
|
|
6f3c5c44a5 | ||
|
|
67b32ca14d | ||
|
|
bffaf477ea | ||
|
|
cba45ece12 | ||
|
|
3290d7f401 | ||
|
|
ef3827376d | ||
|
|
4ae8a91051 | ||
|
|
ff4819ac68 | ||
|
|
58af133853 | ||
|
|
8199cd2118 | ||
|
|
b74b76de75 | ||
|
|
9f02b80cf1 | ||
|
|
d3e4e81168 | ||
|
|
a1b1498106 | ||
|
|
38195c0a8f | ||
|
|
5fa11e4278 | ||
|
|
c39b52fcb5 | ||
|
|
ec65fc1e65 | ||
|
|
32d8646d96 | ||
|
|
e8835fc2b1 | ||
|
|
1158851ea7 | ||
|
|
054a06c5dc | ||
|
|
9c61d2eb22 | ||
|
|
d66b6cfee6 | ||
|
|
89c0427b2f | ||
|
|
3fec10d447 | ||
|
|
e1ab9e959e | ||
|
|
f8b4981bfe | ||
|
|
130669a2f9 | ||
|
|
c2c79ee231 | ||
|
|
86f10bd702 | ||
|
|
2bbac3ae9d | ||
|
|
b496a06fc0 | ||
|
|
f865a6cb37 | ||
|
|
89186ea4f8 | ||
|
|
fb19985186 | ||
|
|
e889b1d5e5 | ||
|
|
ebc6e1221a | ||
|
|
233bf856f4 | ||
|
|
2b91dcb700 | ||
|
|
9d2e333fd6 | ||
|
|
013ad1069c | ||
|
|
ec059717f6 | ||
|
|
418566db3f | ||
|
|
bca843e06c | ||
|
|
7be9463be6 | ||
|
|
51712bc7d6 | ||
|
|
7b889d22d2 | ||
|
|
30a79a1278 | ||
|
|
e8aad989ec | ||
|
|
4006d64d60 | ||
|
|
d792d99375 | ||
|
|
0f92dee2c4 | ||
|
|
b11036a2e5 | ||
|
|
f04efede15 | ||
|
|
01de69a250 | ||
|
|
b842aa11e5 | ||
|
|
ff01078b60 | ||
|
|
f0dfdf6720 | ||
|
|
e26d731382 | ||
|
|
d684d3e559 | ||
|
|
47c54cb998 | ||
|
|
592cb2b3ec | ||
|
|
f5a7871a2e | ||
|
|
ec411fa0db | ||
|
|
7f35498919 | ||
|
|
297205ff91 | ||
|
|
6f99381d13 | ||
|
|
45a42b00ad | ||
|
|
6586858284 | ||
|
|
5da79cd5ca | ||
|
|
19cce15e54 | ||
|
|
62b8e4c46f | ||
|
|
74b7d27408 | ||
|
|
a60ffc0702 | ||
|
|
32526d77b8 | ||
|
|
a850a9bb83 | ||
|
|
13c971b171 | ||
|
|
5711a9e148 | ||
|
|
a0f40c2dfb | ||
|
|
34636016eb | ||
|
|
95027e329c | ||
|
|
c9778a73c7 | ||
|
|
628d2a0a0a | ||
|
|
cc0ff1ec1f | ||
|
|
1cee7e43ed | ||
|
|
cd53fc6318 | ||
|
|
5e81c63d6e | ||
|
|
479b7a3f94 | ||
|
|
f7cfee77c9 | ||
|
|
65a8126a13 | ||
|
|
a39bc102d5 | ||
|
|
81d930c4d2 | ||
|
|
6839623061 | ||
|
|
7de2809d42 | ||
|
|
98ec6b6886 | ||
|
|
04827f00cc | ||
|
|
660bfc6578 | ||
|
|
1152cd5537 | ||
|
|
ae98f6276e | ||
|
|
7424a5987b | ||
|
|
2d08749207 | ||
|
|
db50e98ae3 | ||
|
|
7ec0925c69 | ||
|
|
df1361aa43 | ||
|
|
6db04c86df | ||
|
|
bec80b443c | ||
|
|
333501fe55 | ||
|
|
063553d4f7 | ||
|
|
19b9e50281 | ||
|
|
2a68b4aeff | ||
|
|
5e25150521 | ||
|
|
cb183d3645 | ||
|
|
96bce301a0 | ||
|
|
360f5b7538 | ||
|
|
16fb1018a2 | ||
|
|
ad6fe855a9 | ||
|
|
c50c97d149 | ||
|
|
8b98ae15f0 | ||
|
|
261f84d5f5 | ||
|
|
29989ac24e | ||
|
|
7494fa6bc9 | ||
|
|
62e50dbe52 | ||
|
|
30c090ba2d | ||
|
|
7ff82e6043 | ||
|
|
61d3e2fb49 | ||
|
|
fb18c1a0be | ||
|
|
17456482d6 | ||
|
|
a0431e1912 | ||
|
|
f30e8497b2 | ||
|
|
ea447365fa | ||
|
|
60989d6766 | ||
|
|
06495bc45d | ||
|
|
26067916b3 | ||
|
|
c36ee4852b | ||
|
|
2cb992ad44 | ||
|
|
84ec5aea26 | ||
|
|
ee5aac8008 | ||
|
|
444c13c6ea | ||
|
|
083b7be6c0 | ||
|
|
e24854558f | ||
|
|
e4314cf426 | ||
|
|
69b9b0bbc0 | ||
|
|
3c1088f030 | ||
|
|
4106e4e45c | ||
|
|
05f143db2b | ||
|
|
64aeaeeeea | ||
|
|
61db37ab0d | ||
|
|
f9c4d921e7 | ||
|
|
ca099df573 | ||
|
|
28b584b8bc | ||
|
|
70449e694d | ||
|
|
8395ea552d | ||
|
|
dc66452633 | ||
|
|
53ce44ac91 | ||
|
|
c7c3243bbc | ||
|
|
8bdd77d33d | ||
|
|
acd7d0db3a | ||
|
|
2bfadb8a3c | ||
|
|
0912e4af7b | ||
|
|
5aa5c48018 | ||
|
|
8cdd998f79 | ||
|
|
050d4d6b25 | ||
|
|
366cd11238 | ||
|
|
58d6443331 | ||
|
|
101b8afb56 | ||
|
|
5df5c47945 | ||
|
|
a04740ba86 | ||
|
|
425ad93ac5 | ||
|
|
b0067fee51 | ||
|
|
732c3dae89 | ||
|
|
d45bd67cae | ||
|
|
9200de5032 | ||
|
|
8a44144c20 | ||
|
|
ee82c70582 | ||
|
|
c87e8e606b | ||
|
|
37a50dd953 | ||
|
|
a2669a3084 | ||
|
|
77da22f4dd | ||
|
|
7830ffe202 | ||
|
|
1c9e20d59f | ||
|
|
3fbbff5a47 | ||
|
|
c22efc2c3d | ||
|
|
8c0281bf70 | ||
|
|
320edac286 | ||
|
|
d49878371d | ||
|
|
d2575a5d9b | ||
|
|
ea6cf72580 | ||
|
|
2118155b37 | ||
|
|
ba4f5bb71f | ||
|
|
d5a74a5a8b | ||
|
|
23be1df360 | ||
|
|
b5c1a1da4c | ||
|
|
c11e784f51 | ||
|
|
06f51c8f9c | ||
|
|
181bcbbda6 | ||
|
|
d008ead6a4 | ||
|
|
75924be958 | ||
|
|
b1a6e3f8a2 | ||
|
|
06712a6041 | ||
|
|
720a4bc4a2 | ||
|
|
7fd93645b3 | ||
|
|
62b16339a9 | ||
|
|
9a2f1a36ba | ||
|
|
fcbfbca6d0 | ||
|
|
f2bca9491c | ||
|
|
b48f309ab6 | ||
|
|
0b1be3e63b | ||
|
|
95cc4d3a73 | ||
|
|
497eeeb2e0 | ||
|
|
4be21ca249 | ||
|
|
e8598e214e | ||
|
|
54b1d65e3c | ||
|
|
f7648496d3 | ||
|
|
59a57c7197 | ||
|
|
5659b26827 | ||
|
|
ee4443aaf0 | ||
|
|
839dcad358 | ||
|
|
d67933ab49 | ||
|
|
248a05a916 | ||
|
|
2c141579dd | ||
|
|
0eb3f6b952 | ||
|
|
68b0f80fce | ||
|
|
93489529a3 | ||
|
|
511be74e74 | ||
|
|
bdee067803 | ||
|
|
32156cace3 | ||
|
|
30688114be | ||
|
|
34088bcc17 | ||
|
|
07835766cc | ||
|
|
8c129c10af | ||
|
|
251851ec6a | ||
|
|
049a669186 | ||
|
|
d29f13bae9 | ||
|
|
c758355df9 | ||
|
|
79d97a83af | ||
|
|
85bd47c240 | ||
|
|
473ead9616 | ||
|
|
cf2850933c | ||
|
|
ff2564c57a | ||
|
|
91d3848246 | ||
|
|
c031f0b45e | ||
|
|
fdbb9568ae | ||
|
|
d817883459 | ||
|
|
12255979ac | ||
|
|
366b61850b | ||
|
|
89be6bd183 | ||
|
|
e120331a2c | ||
|
|
cb8a212d96 | ||
|
|
7aec431ac5 | ||
|
|
d19681dea1 | ||
|
|
63ce2a14fe | ||
|
|
f435ebb110 | ||
|
|
843f001bf6 | ||
|
|
bf2299daf8 | ||
|
|
164930d0dd | ||
|
|
387dbac809 | ||
|
|
3b661e5a99 | ||
|
|
90c1c0e655 | ||
|
|
21d8e7695b | ||
|
|
0d28165c04 | ||
|
|
ee31bfbcd4 | ||
|
|
1acc452cfe | ||
|
|
1375e1feee | ||
|
|
2187adf59a | ||
|
|
0dcb315d9d | ||
|
|
327ccbd0c9 | ||
|
|
f571d400e6 | ||
|
|
293aa52335 | ||
|
|
ca178ae9a7 | ||
|
|
d0c810e418 | ||
|
|
d496d2caeb | ||
|
|
e70b75c350 | ||
|
|
a50befeda5 | ||
|
|
e2616e8039 | ||
|
|
904266debe | ||
|
|
72d5783795 | ||
|
|
d699fb1473 | ||
|
|
fab1a6c33a | ||
|
|
be73c30194 | ||
|
|
451646fe4f | ||
|
|
decc919991 | ||
|
|
e0b4005921 | ||
|
|
3ef36e7534 | ||
|
|
1949e1e1e9 | ||
|
|
3358382358 | ||
|
|
3eca3ecd75 | ||
|
|
140c6b91b0 | ||
|
|
a5315ec240 | ||
|
|
93f1656e0b | ||
|
|
f1d006c236 | ||
|
|
b0b5a96694 | ||
|
|
7dbe9a85f4 | ||
|
|
d2c39528d5 | ||
|
|
0420543c94 | ||
|
|
aae0db902b | ||
|
|
88dae7cef7 | ||
|
|
e5cb17e934 | ||
|
|
9d609805f2 | ||
|
|
e2b9ca8254 | ||
|
|
e16a2fe8af | ||
|
|
22d61a533d | ||
|
|
af408bb45f | ||
|
|
24bfbc06f0 | ||
|
|
5eb9f353b5 | ||
|
|
473ce15f47 | ||
|
|
eb9cfbaed6 | ||
|
|
faeb037ff9 | ||
|
|
07602f697d | ||
|
|
11abb0fdb1 | ||
|
|
deeb2fa543 | ||
|
|
ef8d5ff11e | ||
|
|
91f3e07b83 | ||
|
|
c29bdbdacb | ||
|
|
a20d104d2f | ||
|
|
a61dd8ac17 | ||
|
|
7ee9a690ea | ||
|
|
5ba94c6c41 | ||
|
|
9fa855c837 | ||
|
|
9251007574 | ||
|
|
cc73b984cb | ||
|
|
548ef97c32 | ||
|
|
ed8a486726 | ||
|
|
1ab0911fc8 | ||
|
|
bdbaea7294 | ||
|
|
5cfd1f6fb2 | ||
|
|
5eda67381f | ||
|
|
2c8b8bfaf2 | ||
|
|
8f3159751a | ||
|
|
4b05e55b29 | ||
|
|
3d3c13fcd0 | ||
|
|
88e1d8a8cf | ||
|
|
e007db34e2 | ||
|
|
f9f06d2c02 | ||
|
|
234f7d00c8 | ||
|
|
9924553da5 | ||
|
|
df38d7e3ed | ||
|
|
44dd061619 | ||
|
|
7603a932b1 | ||
|
|
138e7acc13 | ||
|
|
e863d3e7e5 | ||
|
|
c8e401f5ed | ||
|
|
3ba20a8e28 | ||
|
|
ebae63752f | ||
|
|
8bc73901cf | ||
|
|
b4f70d9244 | ||
|
|
21e9f2bba3 | ||
|
|
881f4e3d6a | ||
|
|
b141945add | ||
|
|
1c67d6802d | ||
|
|
5da8c86ec7 | ||
|
|
10a2d59ec1 | ||
|
|
34e8360b10 | ||
|
|
ca259ee4c3 | ||
|
|
2143952a1e | ||
|
|
6bab6e7151 | ||
|
|
d217c2e295 | ||
|
|
52bf0faaa5 | ||
|
|
3f3f2bfc61 | ||
|
|
f050864fb4 | ||
|
|
db11fc35f4 | ||
|
|
f47a2b10c0 | ||
|
|
344b4e7d60 | ||
|
|
7a23372489 | ||
|
|
9da15a8e58 | ||
|
|
50e0e4a07b | ||
|
|
5e1562ae4c | ||
|
|
8a0ed49623 |
@@ -3206,7 +3206,8 @@
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3755203?v=4",
|
||||
"profile": "https://github.com/swift2512",
|
||||
"contributions": [
|
||||
"bug"
|
||||
"bug",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -4180,6 +4181,60 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lukaskraic",
|
||||
"name": "Lukas Kraic",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/58696401?v=4",
|
||||
"profile": "https://github.com/lukaskraic",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mckaygerhard",
|
||||
"name": "Герхард PICCORO Lenz McKAY ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1571724?v=4",
|
||||
"profile": "https://github-readme-stats.vercel.app/api?username=mckaygerhard",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "FlorestanII",
|
||||
"name": "Johannes Pollitt",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/15015119?v=4",
|
||||
"profile": "https://github.com/FlorestanII",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "strobelm",
|
||||
"name": "Michael Strobel",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/14185442?v=4",
|
||||
"profile": "https://strobelm.de",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nickwest",
|
||||
"name": "Nicky West",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/634790?v=4",
|
||||
"profile": "http://nickwest.me",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "akaspeh1",
|
||||
"name": "akaspeh1",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1347327?v=4",
|
||||
"profile": "https://github.com/akaspeh1",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ PUBLIC_FILESYSTEM_DISK=local_public
|
||||
# --------------------------------------------
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=db
|
||||
DB_SOCKET=null
|
||||
DB_PORT='3306'
|
||||
DB_DATABASE=snipeit
|
||||
DB_USERNAME=snipeit
|
||||
@@ -168,6 +169,7 @@ AWS_DEFAULT_REGION=null
|
||||
LOGIN_MAX_ATTEMPTS=5
|
||||
LOGIN_LOCKOUT_DURATION=60
|
||||
RESET_PASSWORD_LINK_EXPIRES=900
|
||||
INVITE_PASSWORD_LINK_EXPIRES=1500
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: MISC
|
||||
|
||||
11
.env.example
11
.env.example
@@ -24,6 +24,7 @@ PUBLIC_FILESYSTEM_DISK=local_public
|
||||
# --------------------------------------------
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_SOCKET=null
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=null
|
||||
DB_USERNAME=null
|
||||
@@ -174,6 +175,7 @@ LOGIN_AUTOCOMPLETE=false
|
||||
RESET_PASSWORD_LINK_EXPIRES=15
|
||||
PASSWORD_CONFIRM_TIMEOUT=10800
|
||||
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
|
||||
INVITE_PASSWORD_LINK_EXPIRES=1500
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: MISC
|
||||
@@ -188,14 +190,21 @@ APP_ALLOW_INSECURE_HOSTS=false
|
||||
GOOGLE_MAPS_API=
|
||||
LDAP_MEM_LIM=500M
|
||||
LDAP_TIME_LIM=600
|
||||
BACKUP_TIME_LIMIT=600
|
||||
IMPORT_TIME_LIMIT=600
|
||||
IMPORT_MEMORY_LIMIT=500M
|
||||
REPORT_TIME_LIMIT=12000
|
||||
REQUIRE_SAML=false
|
||||
API_THROTTLE_PER_MINUTE=120
|
||||
CSV_ESCAPE_FORMULAS=true
|
||||
LIVEWIRE_URL_PREFIX=null
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SAML SETTINGS
|
||||
# --------------------------------------------
|
||||
REQUIRE_SAML=false
|
||||
SAML_KEY_SIZE=2048
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: HASHING
|
||||
# --------------------------------------------
|
||||
|
||||
163
.github/ISSUE_TEMPLATE/Bug-Report.yml
vendored
Normal file
163
.github/ISSUE_TEMPLATE/Bug-Report.yml
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
name: Bug Report
|
||||
description: File a bug report.
|
||||
title: "[Bug]: "
|
||||
projects: ["grokability/snipe-it"]
|
||||
type: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Most issues are documented in the [Snipe-IT repository's issues](https://github.com/grokability/snipe-it/issues) or in the official [Common Issues section of the Documentation](https://snipe-it.readme.io/docs/common-issues#/) and are due to the following:
|
||||
|
||||
- `.env` misconfiguration
|
||||
- [Server Permissions](https://snipe-it.readme.io/docs/debugging-permissions#/)
|
||||
- [Database Migrations](https://snipe-it.readme.io/docs/database-issues#run-migrations)
|
||||
|
||||
Please make sure you've checked these resources before submitting a new issue. If you find an existing issue, please add your context to it instead of opening a new issue. If your issue is more of a question, consider [opening a new discussion](https://github.com/grokability/snipe-it/discussions) or [pop by our Discord](https://discord.gg/yZFtShAcKk) instead of creating an issue.
|
||||
|
||||
**Please write your bug report in English.** You can use tools like [DeepL](https://www.deepl.com) or [Google Translate](https://translate.google.com/) to translate if necessary.
|
||||
|
||||
**If you choose to upload screenshots or videos (which we always encourage), please make sure they do not contain any sensitive information.**
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Snipe-IT Version
|
||||
description: What version of Snipe-IT are you seeing this issue on? You can find the version number in the footer of any page in Snipe-IT.
|
||||
placeholder: ex. v8.3.2 - build 19577 (master)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: php-version
|
||||
attributes:
|
||||
label: PHP Version
|
||||
description: What version of PHP are you running? You can find the version of PHP your webserver is running in the `Admin Settings` section in the footer, and the cli version by running `php -v` via command line .
|
||||
placeholder: ex. v8.3.1 (web), PHP 8.4.12 (cli)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: composer-version
|
||||
attributes:
|
||||
label: Composer Version
|
||||
description: What version of composer are you running? You can find the version number by running `composer --version`.
|
||||
placeholder: ex. 2.8.10
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: db-version
|
||||
attributes:
|
||||
label: MySQL/MariaDB version
|
||||
description: What database are you using, and what version?
|
||||
placeholder: ex. MySQL 5.7
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
label: How did you install Snipe-IT?
|
||||
options:
|
||||
- Git install
|
||||
- Manual install (downloading zip/tar.gz)
|
||||
- Docker
|
||||
- install.sh
|
||||
- Hosted by Grokability
|
||||
- Other
|
||||
- Not sure
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: upgrade-or-fresh
|
||||
attributes:
|
||||
label: Is this a fresh install or an upgrade?
|
||||
options:
|
||||
- Fresh install
|
||||
- Upgrade
|
||||
- NA
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see! (Be nice!)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Other
|
||||
- type: dropdown
|
||||
id: on-demo
|
||||
attributes:
|
||||
label: Can you reproduce this on the public demo?
|
||||
description: You can check this at https://demo.snipeitapp.com.
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
- N/A
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: fmcs
|
||||
attributes:
|
||||
label: Do you have full multiple company support enabled?
|
||||
description: You can check this in your Snipe-IT installation at `Admin Settings > General Settings > Scoping`.
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: fmcs-location
|
||||
attributes:
|
||||
label: If you have full multiple company support enabled, do you have location scoping to company enabled?
|
||||
description: You can check this in your Snipe-IT installation at `Admin Settings > General Settings > Scoping`.
|
||||
options:
|
||||
- 'Yes'
|
||||
- 'No'
|
||||
- I do not have full multiple company support enabled
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: server-logs
|
||||
attributes:
|
||||
label: Application log output
|
||||
description: Please copy and paste any relevant log output from `storage/logs/laravel.log`. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: browser-logs
|
||||
attributes:
|
||||
label: Browser console output
|
||||
description: Please copy and paste any relevant log output from your browser console. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: checkboxes
|
||||
id: common-issues
|
||||
attributes:
|
||||
label: Common Issues
|
||||
description: Please make sure you have done the following before submitting your issue.
|
||||
options:
|
||||
- label: I have searched this repo for existing issues related to my issue (including closed issues)
|
||||
required: true
|
||||
- label: My APP_URL is set correctly in my .env file (including http or https and no trailing slash)
|
||||
required: true
|
||||
- label: I have searched the official Snipe-IT documentation and have checked the Common Issues documentation (where applicable)
|
||||
required: true
|
||||
- label: I have run database migrations (where applicable).
|
||||
required: true
|
||||
- label: I have attached screenshots and/or videos of the issue (where applicable)
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/grokability/snipe-it/blob/master/CODE_OF_CONDUCT.md).
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
38
.github/ISSUE_TEMPLATE/Feature-Request.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/Feature-Request.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Feature Request
|
||||
description: Request a new feature.
|
||||
title: "[Feature]: "
|
||||
projects: ["grokability/snipe-it"]
|
||||
type: feature
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request! Please make sure to search the existing issues in this repository to see if your feature has already been requested, and feel free to add your context to any existing requests.
|
||||
|
||||
**Please write your issue in English.** You can use tools like [DeepL](https://www.deepl.com) or [Google Translate](https://translate.google.com/) to translate if necessary.
|
||||
|
||||
**If you choose to upload screenshots or videos (which we always encourage), please make sure they do not contain any sensitive information.**
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Snipe-IT Version
|
||||
description: What version of Snipe-IT are you currently running? You can find the version number in the footer of any page in Snipe-IT.
|
||||
placeholder: ex. v8.3.1 - build 19577 (master)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: How can we help?
|
||||
description: Let us know in detail what feature you'd like to see added. While we can't promise to implement every feature request, we do read every one and take them into consideration when planning future releases.
|
||||
placeholder: Tell us what you'd like to see in Snipe-IT! (Be nice!)
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Code of Conduct
|
||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/grokability/snipe-it/blob/master/CODE_OF_CONDUCT.md).
|
||||
options:
|
||||
- label: I agree to follow this project's Code of Conduct
|
||||
required: true
|
||||
8
.github/workflows/SA-codeql.yml
vendored
8
.github/workflows/SA-codeql.yml
vendored
@@ -26,14 +26,14 @@ jobs:
|
||||
language: [ 'javascript' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
6
.github/workflows/codacy-analysis.yml
vendored
6
.github/workflows/codacy-analysis.yml
vendored
@@ -32,11 +32,11 @@ jobs:
|
||||
steps:
|
||||
# Checkout the repository to the GitHub Actions runner
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
|
||||
- name: Run Codacy Analysis CLI
|
||||
uses: codacy/codacy-analysis-cli-action@v4.4.5
|
||||
uses: codacy/codacy-analysis-cli-action@v4.4.7
|
||||
with:
|
||||
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
|
||||
# You can also omit the token and run the tools that support default configurations
|
||||
@@ -52,6 +52,6 @@ jobs:
|
||||
|
||||
# Upload the SARIF file generated in the previous step
|
||||
- name: Upload SARIF results file
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
2
.github/workflows/crowdin-upload.yml
vendored
2
.github/workflows/crowdin-upload.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Crowdin push
|
||||
uses: crowdin/github-action@v2
|
||||
|
||||
2
.github/workflows/docker-alpine.yml
vendored
2
.github/workflows/docker-alpine.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Setup Docker Buildx
|
||||
|
||||
2
.github/workflows/docker-ubuntu.yml
vendored
2
.github/workflows/docker-ubuntu.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Setup Docker Buildx
|
||||
|
||||
2
.github/workflows/dockerhub-description.yml
vendored
2
.github/workflows/dockerhub-description.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Docker Hub Description
|
||||
uses: grokability/dockerhub-description@7ea9d275c7cdbe2b676a093a0308c50665e3b8b4
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
issues: write
|
||||
# pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
debug-only: true
|
||||
ascending: true
|
||||
|
||||
14
.github/workflows/tests-mysql.yml
vendored
14
.github/workflows/tests-mysql.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
php-version: "${{ matrix.php-version }}"
|
||||
coverage: none
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
@@ -76,4 +76,16 @@ jobs:
|
||||
DB_DATABASE: snipeit
|
||||
DB_PORT: ${{ job.services.mysql.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
LOG_CHANNEL: single
|
||||
LOG_LEVEL: debug
|
||||
run: php artisan test
|
||||
|
||||
- name: Upload Laravel logs as artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
|
||||
path: |
|
||||
storage/logs/*.log
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
14
.github/workflows/tests-postgres.yml
vendored
14
.github/workflows/tests-postgres.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
php-version: "${{ matrix.php-version }}"
|
||||
coverage: none
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
@@ -75,4 +75,16 @@ jobs:
|
||||
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
|
||||
DB_USERNAME: snipeit
|
||||
DB_PASSWORD: password
|
||||
LOG_CHANNEL: single
|
||||
LOG_LEVEL: debug
|
||||
run: php artisan test
|
||||
|
||||
- name: Upload Laravel logs as artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
|
||||
path: |
|
||||
storage/logs/*.log
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
14
.github/workflows/tests-sqlite.yml
vendored
14
.github/workflows/tests-sqlite.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
php-version: "${{ matrix.php-version }}"
|
||||
coverage: none
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
@@ -61,4 +61,16 @@ jobs:
|
||||
- name: Execute tests (Unit and Feature tests) via PHPUnit
|
||||
env:
|
||||
DB_CONNECTION: sqlite
|
||||
LOG_CHANNEL: single
|
||||
LOG_LEVEL: debug
|
||||
run: php artisan test
|
||||
|
||||
- name: Upload Laravel logs as artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
|
||||
path: |
|
||||
storage/logs/*.log
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
@@ -52,7 +52,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
|
||||
| [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") | [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") | [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") | [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | [<img src="https://avatars.githubusercontent.com/u/6136439?v=4" width="110px;"/><br /><sub>Darren Rainey</sub>](https://darrenraineys.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [<img src="https://avatars.githubusercontent.com/u/133033121?v=4" width="110px;"/><br /><sub>maciej-poleszczyk</sub>](https://github.com/maciej-poleszczyk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") | [<img src="https://avatars.githubusercontent.com/u/143394709?v=4" width="110px;"/><br /><sub>Sebastian Groß</sub>](https://github.com/sgross-emlix)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sgross-emlix "Code") | [<img src="https://avatars.githubusercontent.com/u/41107778?v=4" width="110px;"/><br /><sub>Anouar Touati</sub>](https://github.com/AnouarTouati)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AnouarTouati "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") [💻](https://github.com/snipe/snipe-it/commits?author=swift2512 "Code") | [<img src="https://avatars.githubusercontent.com/u/6136439?v=4" width="110px;"/><br /><sub>Darren Rainey</sub>](https://darrenraineys.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [<img src="https://avatars.githubusercontent.com/u/133033121?v=4" width="110px;"/><br /><sub>maciej-poleszczyk</sub>](https://github.com/maciej-poleszczyk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") | [<img src="https://avatars.githubusercontent.com/u/143394709?v=4" width="110px;"/><br /><sub>Sebastian Groß</sub>](https://github.com/sgross-emlix)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sgross-emlix "Code") | [<img src="https://avatars.githubusercontent.com/u/41107778?v=4" width="110px;"/><br /><sub>Anouar Touati</sub>](https://github.com/AnouarTouati)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AnouarTouati "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/25596663?v=4" width="110px;"/><br /><sub>aHVzY2g</sub>](https://github.com/aHVzY2g)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aHVzY2g "Code") | [<img src="https://avatars.githubusercontent.com/u/13408130?v=4" width="110px;"/><br /><sub>林博仁 Buo-ren Lin</sub>](https://brlin.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brlin-tw "Code") | [<img src="https://avatars.githubusercontent.com/u/18550946?v=4" width="110px;"/><br /><sub>Adugna Gizaw</sub>](https://orbalia.pythonanywhere.com/)<br />[🌍](#translation-addex12 "Translation") | [<img src="https://avatars.githubusercontent.com/u/760989?v=4" width="110px;"/><br /><sub>Jesse Ostrander</sub>](https://github.com/jostrander)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jostrander "Code") | [<img src="https://avatars.githubusercontent.com/u/31522486?v=4" width="110px;"/><br /><sub>James M</sub>](https://github.com/azmcnutt)<br />[💻](https://github.com/snipe/snipe-it/commits?author=azmcnutt "Code") | [<img src="https://avatars.githubusercontent.com/u/5183146?v=4" width="110px;"/><br /><sub>Fiala06</sub>](https://github.com/Fiala06)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Fiala06 "Code") | [<img src="https://avatars.githubusercontent.com/u/28693782?v=4" width="110px;"/><br /><sub>Nathan Taylor</sub>](https://github.com/ntaylor-86)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntaylor-86 "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/16699443?v=4" width="110px;"/><br /><sub>fvollmer</sub>](https://github.com/fvollmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fvollmer "Code") | [<img src="https://avatars.githubusercontent.com/u/109086466?v=4" width="110px;"/><br /><sub>36864</sub>](https://github.com/36864)<br />[💻](https://github.com/snipe/snipe-it/commits?author=36864 "Code") | [<img src="https://avatars.githubusercontent.com/u/365751?v=4" width="110px;"/><br /><sub>Daniel O'Connor</sub>](http://clockwerx.blogspot.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CloCkWeRX "Code") | [<img src="https://avatars.githubusercontent.com/u/102852568?v=4" width="110px;"/><br /><sub>BeatSpark</sub>](https://github.com/BeatSpark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BeatSpark "Code") | [<img src="https://avatars.githubusercontent.com/u/59203607?v=4" width="110px;"/><br /><sub>mrdahbi</sub>](https://github.com/mrdahbi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrdahbi "Code") | [<img src="https://avatars.githubusercontent.com/u/6661332?v=4" width="110px;"/><br /><sub>Fabian Schmid</sub>](http://sr.solutions)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chfsx "Code") | [<img src="https://avatars.githubusercontent.com/u/1288116?v=4" width="110px;"/><br /><sub>Chris Olin</sub>](https://www.chrisolin.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=realchrisolin "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/3803132?v=4" width="110px;"/><br /><sub>Dan</sub>](https://github.com/mnemonicly)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mnemonicly "Code") | [<img src="https://avatars.githubusercontent.com/u/43917728?v=4" width="110px;"/><br /><sub>Nebel</sub>](https://github.com/NebelKreis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NebelKreis "Code") | [<img src="https://avatars.githubusercontent.com/u/132433803?v=4" width="110px;"/><br /><sub>test1337ahp</sub>](https://github.com/test1337ahp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=test1337ahp "Code") | [<img src="https://avatars.githubusercontent.com/u/1916566?v=4" width="110px;"/><br /><sub>Jonathon Reinhart</sub>](https://github.com/JonathonReinhart)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JonathonReinhart "Code") | [<img src="https://avatars.githubusercontent.com/u/484742?v=4" width="110px;"/><br /><sub>aranar-pro</sub>](https://github.com/aranar-pro)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aranar-pro "Code") | [<img src="https://avatars.githubusercontent.com/u/27019397?v=4" width="110px;"/><br /><sub>Phil</sub>](https://github.com/phil-flip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phil-flip "Code") | [<img src="https://avatars.githubusercontent.com/u/6473460?v=4" width="110px;"/><br /><sub>Steffy Fort</sub>](https://fe80.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fe80 "Code") |
|
||||
@@ -67,7 +67,8 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
|
||||
| [<img src="https://avatars.githubusercontent.com/u/80526133?v=4" width="110px;"/><br /><sub>AlexanderWPapyrus</sub>](https://github.com/AlexanderWPapyrus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AlexanderWPapyrus "Code") | [<img src="https://avatars.githubusercontent.com/u/306231?v=4" width="110px;"/><br /><sub>Alexandr Hacicheant</sub>](https://github.com/disc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=disc "Code") | [<img src="https://avatars.githubusercontent.com/u/3032891?v=4" width="110px;"/><br /><sub>Hex</sub>](https://hex128.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hex128 "Code") | [<img src="https://avatars.githubusercontent.com/u/8697942?v=4" width="110px;"/><br /><sub>Arunas Skirius</sub>](https://github.com/arukompas)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arukompas "Code") | [<img src="https://avatars.githubusercontent.com/u/104396?v=4" width="110px;"/><br /><sub>Ben Periton</sub>](https://github.com/benperiton)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benperiton "Code") | [<img src="https://avatars.githubusercontent.com/u/11906832?v=4" width="110px;"/><br /><sub>Byron Wolfman</sub>](https://wolfman.dev/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=byronwolfman "Code") | [<img src="https://avatars.githubusercontent.com/u/56485508?v=4" width="110px;"/><br /><sub>Calvin</sub>](https://github.com/CalvinSchwartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CalvinSchwartz "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/181059?v=4" width="110px;"/><br /><sub>Juan Font</sub>](https://github.com/juanfont)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juanfont "Code") | [<img src="https://avatars.githubusercontent.com/u/13137708?v=4" width="110px;"/><br /><sub>Juho Taipale</sub>](https://github.com/juhotaipale)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juhotaipale "Code") | [<img src="https://avatars.githubusercontent.com/u/1007419?v=4" width="110px;"/><br /><sub>Korvin Szanto</sub>](https://github.com/KorvinSzanto)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KorvinSzanto "Code") | [<img src="https://avatars.githubusercontent.com/u/8513053?v=4" width="110px;"/><br /><sub>Lewis Foster</sub>](https://lewisfoster.foo/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sniff122 "Code") | [<img src="https://avatars.githubusercontent.com/u/33877541?v=4" width="110px;"/><br /><sub>Logan Swartzendruber</sub>](https://github.com/loganswartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=loganswartz "Code") | [<img src="https://avatars.githubusercontent.com/u/1156208?v=4" width="110px;"/><br /><sub>Lorenzo P.</sub>](https://github.com/lopezio)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lopezio "Code") | [<img src="https://avatars.githubusercontent.com/u/33946590?v=4" width="110px;"/><br /><sub>Lukas Jung</sub>](https://github.com/m4us1ne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=m4us1ne "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/10965027?v=4" width="110px;"/><br /><sub>Ellie</sub>](https://leafedfox.xyz/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [<img src="https://avatars.githubusercontent.com/u/20960555?v=4" width="110px;"/><br /><sub>GA Stamper</sub>](https://github.com/gastamper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [<img src="https://avatars.githubusercontent.com/u/206553556?v=4" width="110px;"/><br /><sub>Guillaume Lefranc</sub>](https://github.com/gl-pup)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [<img src="https://avatars.githubusercontent.com/u/733892?v=4" width="110px;"/><br /><sub>Hajo Möller</sub>](https://github.com/dasjoe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [<img src="https://avatars.githubusercontent.com/u/3420063?v=4" width="110px;"/><br /><sub>Istvan Basa</sub>](https://github.com/pottom)<br />[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [<img src="https://avatars.githubusercontent.com/u/810824?v=4" width="110px;"/><br /><sub>JJ Asghar</sub>](https://jjasghar.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [<img src="https://avatars.githubusercontent.com/u/40404495?v=4" width="110px;"/><br /><sub>James E. Msenga</sub>](https://github.com/JemCdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [<img src="https://avatars.githubusercontent.com/u/634790?v=4" width="110px;"/><br /><sub>Nicky West</sub>](http://nickwest.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [<img src="https://avatars.githubusercontent.com/u/1347327?v=4" width="110px;"/><br /><sub>akaspeh1</sub>](https://github.com/akaspeh1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") |
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
59
app/Actions/Categories/DestroyCategoryAction.php
Normal file
59
app/Actions/Categories/DestroyCategoryAction.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Categories;
|
||||
|
||||
use App\Exceptions\ItemStillHasAccessories;
|
||||
use App\Exceptions\ItemStillHasAssetModels;
|
||||
use App\Exceptions\ItemStillHasAssets;
|
||||
use App\Exceptions\ItemStillHasComponents;
|
||||
use App\Exceptions\ItemStillHasConsumables;
|
||||
use App\Exceptions\ItemStillHasLicenses;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DestroyCategoryAction
|
||||
{
|
||||
/**
|
||||
* @throws ItemStillHasAssets
|
||||
* @throws ItemStillHasAssetModels
|
||||
* @throws ItemStillHasComponents
|
||||
* @throws ItemStillHasAccessories
|
||||
* @throws ItemStillHasLicenses
|
||||
* @throws ItemStillHasConsumables
|
||||
*/
|
||||
static function run(Category $category): bool
|
||||
{
|
||||
$category->loadCount([
|
||||
'assets as assets_count',
|
||||
'accessories as accessories_count',
|
||||
'consumables as consumables_count',
|
||||
'components as components_count',
|
||||
'licenses as licenses_count',
|
||||
'models as models_count'
|
||||
]);
|
||||
|
||||
if ($category->assets_count > 0) {
|
||||
throw new ItemStillHasAssets($category);
|
||||
}
|
||||
if ($category->accessories_count > 0) {
|
||||
throw new ItemStillHasAccessories($category);
|
||||
}
|
||||
if ($category->consumables_count > 0) {
|
||||
throw new ItemStillHasConsumables($category);
|
||||
}
|
||||
if ($category->components_count > 0) {
|
||||
throw new ItemStillHasComponents($category);
|
||||
}
|
||||
if ($category->licenses_count > 0) {
|
||||
throw new ItemStillHasLicenses($category);
|
||||
}
|
||||
if ($category->models_count > 0) {
|
||||
throw new ItemStillHasAssetModels($category);
|
||||
}
|
||||
|
||||
Storage::disk('public')->delete('categories'.'/'.$category->image);
|
||||
$category->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
63
app/Actions/Manufacturers/DeleteManufacturerAction.php
Normal file
63
app/Actions/Manufacturers/DeleteManufacturerAction.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Manufacturers;
|
||||
|
||||
use App\Exceptions\ItemStillHasAccessories;
|
||||
use App\Exceptions\ItemStillHasAssets;
|
||||
use App\Exceptions\ItemStillHasComponents;
|
||||
use App\Exceptions\ItemStillHasConsumables;
|
||||
use App\Exceptions\ItemStillHasLicenses;
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DeleteManufacturerAction
|
||||
{
|
||||
/**
|
||||
* @throws ItemStillHasAssets
|
||||
* @throws ItemStillHasComponents
|
||||
* @throws ItemStillHasAccessories
|
||||
* @throws ItemStillHasLicenses
|
||||
* @throws ItemStillHasConsumables
|
||||
*/
|
||||
static function run(Manufacturer $manufacturer): bool
|
||||
{
|
||||
$manufacturer->loadCount([
|
||||
'assets as assets_count',
|
||||
'accessories as accessories_count',
|
||||
'consumables as consumables_count',
|
||||
'components as components_count',
|
||||
'licenses as licenses_count',
|
||||
]);
|
||||
|
||||
if ($manufacturer->assets_count > 0) {
|
||||
throw new ItemStillHasAssets($manufacturer);
|
||||
}
|
||||
if ($manufacturer->accessories_count > 0) {
|
||||
throw new ItemStillHasAccessories($manufacturer);
|
||||
}
|
||||
if ($manufacturer->consumables_count > 0) {
|
||||
throw new ItemStillHasConsumables($manufacturer);
|
||||
}
|
||||
if ($manufacturer->components_count > 0) {
|
||||
throw new ItemStillHasComponents($manufacturer);
|
||||
}
|
||||
if ($manufacturer->licenses_count > 0) {
|
||||
throw new ItemStillHasLicenses($manufacturer);
|
||||
}
|
||||
|
||||
if ($manufacturer->image) {
|
||||
try {
|
||||
Storage::disk('public')->delete('manufacturers/'.$manufacturer->image);
|
||||
} catch (\Exception $e) {
|
||||
Log::info($e);
|
||||
}
|
||||
}
|
||||
|
||||
$manufacturer->delete();
|
||||
//dd($manufacturer);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
72
app/Actions/Suppliers/DestroySupplierAction.php
Normal file
72
app/Actions/Suppliers/DestroySupplierAction.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Suppliers;
|
||||
|
||||
use App\Exceptions\ItemStillHasAccessories;
|
||||
use App\Exceptions\ItemStillHasComponents;
|
||||
use App\Exceptions\ItemStillHasConsumables;
|
||||
use App\Models\Supplier;
|
||||
use App\Exceptions\ItemStillHasAssets;
|
||||
use App\Exceptions\ItemStillHasMaintenances;
|
||||
use App\Exceptions\ItemStillHasLicenses;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DestroySupplierAction
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @throws ItemStillHasLicenses
|
||||
* @throws ItemStillHasAssets
|
||||
* @throws ItemStillHasMaintenances
|
||||
* @throws ItemStillHasAccessories
|
||||
* @throws ItemStillHasConsumables
|
||||
* @throws ItemStillHasComponents
|
||||
*/
|
||||
static function run(Supplier $supplier): bool
|
||||
{
|
||||
$supplier->loadCount([
|
||||
'maintenances as maintenances_count',
|
||||
'assets as assets_count',
|
||||
'licenses as licenses_count',
|
||||
'accessories as accessories_count',
|
||||
'consumables as consumables_count',
|
||||
'components as components_count',
|
||||
]);
|
||||
if ($supplier->assets_count > 0) {
|
||||
throw new ItemStillHasAssets($supplier);
|
||||
}
|
||||
|
||||
if ($supplier->maintenances_count > 0) {
|
||||
throw new ItemStillHasMaintenances($supplier);
|
||||
}
|
||||
|
||||
if ($supplier->licenses_count > 0) {
|
||||
throw new ItemStillHasLicenses($supplier);
|
||||
}
|
||||
|
||||
if ($supplier->accessories_count > 0) {
|
||||
throw new ItemStillHasAccessories($supplier);
|
||||
}
|
||||
|
||||
if ($supplier->consumables_count > 0) {
|
||||
throw new ItemStillHasConsumables($supplier);
|
||||
}
|
||||
|
||||
if ($supplier->components_count > 0) {
|
||||
throw new ItemStillHasComponents($supplier);
|
||||
}
|
||||
|
||||
if ($supplier->image) {
|
||||
try {
|
||||
Storage::disk('public')->delete('suppliers/'.$supplier->image);
|
||||
} catch (\Exception $e) {
|
||||
Log::info($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$supplier->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
68
app/Console/Commands/CleanIncorrectCheckoutAcceptances.php
Normal file
68
app/Console/Commands/CleanIncorrectCheckoutAcceptances.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanIncorrectCheckoutAcceptances extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:clean-checkout-acceptances';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = "Delete checkout acceptances for checkouts to non-users";
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$deletions = 0;
|
||||
$skips = 0;
|
||||
|
||||
// This walks *every* checkoutacceptance. That's gnarly. But necessary
|
||||
$this->withProgressBar(CheckoutAcceptance::all(), function ($checkoutAcceptance) use (&$deletions, &$skips) {
|
||||
$item = $checkoutAcceptance->checkoutable;
|
||||
$checkout_to_id = $checkoutAcceptance->assigned_to_id;
|
||||
if(is_null($item)) {
|
||||
$this->info("'Checkoutable' Item is null, going to next record");
|
||||
return; //'false' allegedly breaks execution entirely, so 'true' maybe doesn't? hrm. just straight return maybe?
|
||||
}
|
||||
if(get_class($item) == LicenseSeat::class) {
|
||||
$item = $item->license;
|
||||
}
|
||||
foreach($item->assetlog()->where('action_type','checkout')->get() as $assetlog) {
|
||||
if ($assetlog->target_id == $checkout_to_id && $assetlog->target_type != User::class) {
|
||||
//We have a checkout-to an ID for a non-User, which matches to an ID in the checkout_acceptances table
|
||||
|
||||
//now, let's compare the _times_ - are they close?
|
||||
//I'm picking `created_at` over `action_date` because I'm more interested in when the actionlogs
|
||||
//were _created_, not when they were alleged to have happened - those created_at times need to be within 'X' seconds of
|
||||
//each other (currently 5)
|
||||
if ($assetlog->created_at->diffInSeconds($checkoutAcceptance->created_at, true) <= 5) { //we're allowing for five _ish_ seconds of slop
|
||||
$deletions++;
|
||||
$checkoutAcceptance->forceDelete(); // HARD delete this record; it should have never been
|
||||
return;
|
||||
} else {
|
||||
//$this->info("The two records are too far apart");
|
||||
}
|
||||
} else {
|
||||
//$this->info("No match! checkout to id: " . $checkout_to_id." target_id: ".$assetlog->target_id." target_type: ".$assetlog->target_type);
|
||||
}
|
||||
}
|
||||
$skips++;
|
||||
});
|
||||
$this->error("Final deletion count: $deletions, and skip count: $skips");
|
||||
}
|
||||
}
|
||||
74
app/Console/Commands/CleanOldCheckoutRequests.php
Normal file
74
app/Console/Commands/CleanOldCheckoutRequests.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\CheckoutRequest;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanOldCheckoutRequests extends Command
|
||||
{
|
||||
private int $deletions = 0;
|
||||
private int $skips = 0;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:clean-old-checkout-requests';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Removes checkout requests that reference deleted assets or users.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$requests = CheckoutRequest::with([
|
||||
'user' => function ($query) {
|
||||
$query->withTrashed();
|
||||
},
|
||||
'requestedItem' => function ($query) {
|
||||
$query->withTrashed();
|
||||
},
|
||||
])->get();
|
||||
|
||||
$this->info("Processing {$requests->count()} checkout requests");
|
||||
|
||||
$this->withProgressBar($requests, function ($request) {
|
||||
if ($this->shouldForceDelete($request)) {
|
||||
$request->forceDelete();
|
||||
$this->deletions++;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->shouldSoftDelete($request)) {
|
||||
$request->delete();
|
||||
$this->deletions++;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->skips++;
|
||||
});
|
||||
|
||||
$this->info("Final deletion count: $this->deletions, and skip count: $this->skips");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function shouldForceDelete(CheckoutRequest $request)
|
||||
{
|
||||
// check if the requestable or user relationship is null
|
||||
return !$request->requestable || !$request->user;
|
||||
}
|
||||
|
||||
private function shouldSoftDelete(CheckoutRequest $request)
|
||||
{
|
||||
return $request->requestable->trashed() || $request->user->trashed();
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,8 @@ class LdapSync extends Command
|
||||
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
|
||||
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
|
||||
|
||||
|
||||
// Map the LDAP attributes to the Snipe-IT user fields.
|
||||
$ldap_map = [
|
||||
"username" => Setting::getSettings()->ldap_username_field,
|
||||
"last_name" => Setting::getSettings()->ldap_lname_field,
|
||||
@@ -63,11 +65,17 @@ class LdapSync extends Command
|
||||
"emp_num" => Setting::getSettings()->ldap_emp_num,
|
||||
"email" => Setting::getSettings()->ldap_email,
|
||||
"phone" => Setting::getSettings()->ldap_phone_field,
|
||||
"mobile" => Setting::getSettings()->ldap_mobile,
|
||||
"jobtitle" => Setting::getSettings()->ldap_jobtitle,
|
||||
"address" => Setting::getSettings()->ldap_address,
|
||||
"city" => Setting::getSettings()->ldap_city,
|
||||
"state" => Setting::getSettings()->ldap_state,
|
||||
"zip" => Setting::getSettings()->ldap_zip,
|
||||
"country" => Setting::getSettings()->ldap_country,
|
||||
"location" => Setting::getSettings()->ldap_location,
|
||||
"dept" => Setting::getSettings()->ldap_dept,
|
||||
"manager" => Setting::getSettings()->ldap_manager,
|
||||
"display_name" => Setting::getSettings()->ldap_display_name,
|
||||
];
|
||||
|
||||
$ldap_default_group = Setting::getSettings()->ldap_default_group;
|
||||
@@ -182,7 +190,7 @@ class LdapSync extends Command
|
||||
// Inject location information fields
|
||||
for ($i = 0; $i < $results['count']; $i++) {
|
||||
$results[$i]['ldap_location_override'] = false;
|
||||
$results[$i]['location_id'] = 0;
|
||||
$results[$i]['location_id'] = null;
|
||||
}
|
||||
|
||||
// Grab subsets based on location-specific DNs, and overwrite location for these users.
|
||||
@@ -234,9 +242,11 @@ class LdapSync extends Command
|
||||
}
|
||||
|
||||
|
||||
// Assign the mapped LDAP attributes for each user to the Snipe-IT user fields
|
||||
for ($i = 0; $i < $results['count']; $i++) {
|
||||
$item = [];
|
||||
$item['username'] = $results[$i][$ldap_map["username"]][0] ?? '';
|
||||
$item['display_name'] = $results[$i][$ldap_map["display_name"]][0] ?? '';
|
||||
$item['employee_number'] = $results[$i][$ldap_map["emp_num"]][0] ?? '';
|
||||
$item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? '';
|
||||
$item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? '';
|
||||
@@ -244,8 +254,13 @@ class LdapSync extends Command
|
||||
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
|
||||
$item['location_id'] = $results[$i]['location_id'] ?? '';
|
||||
$item['telephone'] = $results[$i][$ldap_map["phone"]][0] ?? '';
|
||||
$item['mobile'] = $results[$i][$ldap_map["mobile"]][0] ?? '';
|
||||
$item['jobtitle'] = $results[$i][$ldap_map["jobtitle"]][0] ?? '';
|
||||
$item['address'] = $results[$i][$ldap_map["address"]][0] ?? '';
|
||||
$item['city'] = $results[$i][$ldap_map["city"]][0] ?? '';
|
||||
$item['state'] = $results[$i][$ldap_map["state"]][0] ?? '';
|
||||
$item['country'] = $results[$i][$ldap_map["country"]][0] ?? '';
|
||||
$item['zip'] = $results[$i][$ldap_map["zip"]][0] ?? '';
|
||||
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
|
||||
$item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? '';
|
||||
$item['location'] = $results[$i][$ldap_map["location"]][0] ?? '';
|
||||
@@ -278,6 +293,9 @@ class LdapSync extends Command
|
||||
if($ldap_map["username"] != null){
|
||||
$user->username = $item['username'];
|
||||
}
|
||||
if($ldap_map["display_name"] != null){
|
||||
$user->display_name = $item['display_name'];
|
||||
}
|
||||
if($ldap_map["last_name"] != null){
|
||||
$user->last_name = $item['lastname'];
|
||||
}
|
||||
@@ -293,12 +311,27 @@ class LdapSync extends Command
|
||||
if($ldap_map["phone"] != null){
|
||||
$user->phone = $item['telephone'];
|
||||
}
|
||||
if($ldap_map["mobile"] != null){
|
||||
$user->mobile = $item['mobile'];
|
||||
}
|
||||
if($ldap_map["jobtitle"] != null){
|
||||
$user->jobtitle = $item['jobtitle'];
|
||||
}
|
||||
if($ldap_map["address"] != null){
|
||||
$user->address = $item['address'];
|
||||
}
|
||||
if($ldap_map["city"] != null){
|
||||
$user->city = $item['city'];
|
||||
}
|
||||
if($ldap_map["state"] != null){
|
||||
$user->state = $item['state'];
|
||||
}
|
||||
if($ldap_map["country"] != null){
|
||||
$user->country = $item['country'];
|
||||
}
|
||||
if($ldap_map["zip"] != null){
|
||||
$user->zip = $item['zip'];
|
||||
}
|
||||
if($ldap_map["dept"] != null){
|
||||
$user->department_id = $department->id;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use Illuminate\Console\Command;
|
||||
use App\Models\Setting;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use App\Models\Ldap;
|
||||
|
||||
/**
|
||||
* Check if a given ip is in a network
|
||||
@@ -160,7 +161,15 @@ class LdapTroubleshooter extends Command
|
||||
$output[] = "-x";
|
||||
$output[] = "-b ".escapeshellarg($settings->ldap_basedn);
|
||||
$output[] = "-D ".escapeshellarg($settings->ldap_uname);
|
||||
$output[] = "-w ".escapeshellarg(Crypt::Decrypt($settings->ldap_pword));
|
||||
|
||||
try {
|
||||
$w = Crypt::Decrypt($settings->ldap_pword);
|
||||
} catch (\Exception $e) {
|
||||
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$output[] = "-w ". escapeshellarg($w);
|
||||
$output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter));
|
||||
if($settings->ldap_tls) {
|
||||
$this->line("# adding STARTTLS option");
|
||||
@@ -171,6 +180,23 @@ class LdapTroubleshooter extends Command
|
||||
$this->line(implode(" \\\n",$output));
|
||||
exit(0);
|
||||
}
|
||||
|
||||
//PHP Version check for warning
|
||||
$php_version = phpversion();
|
||||
list($major, $minor, $patch) = explode('.', $php_version);
|
||||
if (
|
||||
$major < 8 ||
|
||||
($major == 8 && $minor < 3) ||
|
||||
($major == 8 && $minor == 3 && $patch < 21) ||
|
||||
($major == 8 && $minor == 4 && $patch < 7)
|
||||
) {
|
||||
$this->warn("PHP Version: $php_version WARNING - Versions before 8.3.21 or 8.4.7 will return INCONSISTENT results!");
|
||||
if (!$this->confirm("Are you sure you wish to continue?")) {
|
||||
$this->warn("ABORTING");
|
||||
exit(-1);
|
||||
}
|
||||
}
|
||||
|
||||
if(!$this->option('force')) {
|
||||
$confirmation = $this->confirm('WARNING: This command will make several attempts to connect to your LDAP server. Are you sure this is ok?');
|
||||
if(!$confirmation) {
|
||||
@@ -179,7 +205,7 @@ class LdapTroubleshooter extends Command
|
||||
}
|
||||
}
|
||||
//$this->line(print_r($settings,true));
|
||||
$this->info("STAGE 1: Checking settings");
|
||||
$this->line("STAGE 1: Checking settings");
|
||||
if(!$settings->ldap_enabled) {
|
||||
$this->error("WARNING: Snipe-IT's LDAP setting is not turned on. (That may be OK if you're still trying to figure out settings)");
|
||||
}
|
||||
@@ -210,32 +236,40 @@ class LdapTroubleshooter extends Command
|
||||
$this->info("Determined LDAP hostname to be: ".$parsed['host']);
|
||||
}
|
||||
|
||||
$this->info("Performing DNS lookup of: ".$parsed['host']);
|
||||
$ips = dns_get_record($parsed['host']);
|
||||
$raw_ips = [];
|
||||
|
||||
//$this->info("Host IP is: ".print_r($ips,true));
|
||||
if (inet_pton($parsed['host']) !== false) {
|
||||
$this->line($parsed['host'] . " already looks like an address; skipping DNS lookup");
|
||||
$raw_ips[] = $parsed['host'];
|
||||
} else {
|
||||
$this->line("Performing DNS lookup of: " . $parsed['host']);
|
||||
$ips = dns_get_record($parsed['host']);
|
||||
|
||||
if(!$ips || count($ips) == 0) {
|
||||
$this->error("ERROR: DNS lookup of host: ".$parsed['host']." has failed. ABORTING.");
|
||||
exit(-1);
|
||||
}
|
||||
$this->debugout("IP's? ".print_r($ips,true));
|
||||
foreach($ips as $ip) {
|
||||
if(!isset($ip['ip'])) {
|
||||
continue;
|
||||
//$this->info("Host IP is: ".print_r($ips,true));
|
||||
|
||||
if (!$ips || count($ips) == 0) {
|
||||
$this->error("ERROR: DNS lookup of host: " . $parsed['host'] . " has failed. ABORTING.");
|
||||
exit(-1);
|
||||
}
|
||||
$raw_ips[]=$ip['ip'];
|
||||
if($ip['ip'] == "127.0.0.1") {
|
||||
$this->debugout("IP's? " . print_r($ips, true));
|
||||
foreach ($ips as $ip) {
|
||||
if (!isset($ip['ip'])) {
|
||||
continue;
|
||||
}
|
||||
$raw_ips[] = $ip['ip'];
|
||||
}
|
||||
}
|
||||
foreach ($raw_ips as $ip) {
|
||||
if ($ip == "127.0.0.1") {
|
||||
$this->error("WARNING: Using the localhost IP as the LDAP server. This is usually wrong");
|
||||
}
|
||||
if(ip_in_range($ip['ip'],'10.0.0.0/8') || ip_in_range($ip['ip'],'192.168.0.0/16') || ip_in_range($ip['ip'], '172.16.0.0/12')) {
|
||||
if (ip_in_range($ip, '10.0.0.0/8') || ip_in_range($ip, '192.168.0.0/16') || ip_in_range($ip, '172.16.0.0/12')) {
|
||||
$this->error("WARNING: Using an RFC1918 Private address for LDAP server. This may be correct, but it can be a problem if your Snipe-IT instance is not hosted on your private network");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("STAGE 2: Checking basic network connectivity");
|
||||
$ports = [389,636];
|
||||
$this->line("STAGE 2: Checking basic network connectivity");
|
||||
$ports = [636, 389];
|
||||
if(@$parsed['port'] && !in_array($parsed['port'],$ports)) {
|
||||
$ports[] = $parsed['port'];
|
||||
}
|
||||
@@ -246,7 +280,7 @@ class LdapTroubleshooter extends Command
|
||||
$errstr = '';
|
||||
$timeout = 30.0;
|
||||
$result = '';
|
||||
$this->info("Attempting to connect to port: ".$port." - may take up to $timeout seconds");
|
||||
$this->line("Attempting to connect to port: " . $port . " - may take up to $timeout seconds");
|
||||
try {
|
||||
$result = fsockopen($parsed['host'], $port, $errno, $errstr, 30.0);
|
||||
} catch(Exception $e) {
|
||||
@@ -265,9 +299,9 @@ class LdapTroubleshooter extends Command
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
$this->info("STAGE 3: Determine encryption algorithm, if any");
|
||||
$this->line("STAGE 3: Determine encryption algorithm, if any");
|
||||
|
||||
$ldap_urls = [];
|
||||
$ldap_urls = []; // [url, cert-check?, start_tls?]
|
||||
$pretty_ldap_urls = [];
|
||||
foreach($open_ports as $port) {
|
||||
$this->line("Trying TLS first for port $port");
|
||||
@@ -275,35 +309,46 @@ class LdapTroubleshooter extends Command
|
||||
if($this->test_anonymous_bind($ldap_url)) {
|
||||
$this->info("Anonymous bind succesful to $ldap_url!");
|
||||
$ldap_urls[] = [ $ldap_url, true, false ];
|
||||
$pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ];
|
||||
$pretty_ldap_urls[] = [$ldap_url, "enabled", "n/a (no)"];
|
||||
continue; // TODO - lots of copypasta in these if(test_anonymous_bind()) routines...
|
||||
} else {
|
||||
$this->error("WARNING: Failed to bind to $ldap_url - trying without certificate checks.");
|
||||
}
|
||||
|
||||
if($this->test_anonymous_bind($ldap_url, false)) {
|
||||
$this->info("Anonymous bind succesful to $ldap_url with certifcate-checks disabled");
|
||||
$ldap_urls[] = [ $ldap_url, false, false ];
|
||||
$pretty_ldap_urls[] = [ $ldap_url, "no", "no" ];
|
||||
$this->info("Anonymous bind successful to $ldap_url with certificate-checks disabled");
|
||||
$ldap_urls[] = [$ldap_url, false, false];
|
||||
$pretty_ldap_urls[] = [$ldap_url, "DISABLED", "n/a (no)"];
|
||||
continue;
|
||||
} else {
|
||||
$this->error("WARNING: Failed to bind to $ldap_url with certificate checks disabled. Trying unencrypted with STARTTLS");
|
||||
}
|
||||
|
||||
// now switching to ldap:// URL's from ldaps://
|
||||
$ldap_url = "ldap://".$parsed['host'].":$port";
|
||||
|
||||
if($this->test_anonymous_bind($ldap_url, true, true)) {
|
||||
$this->info("Plain connection to $ldap_url with STARTTLS succesful!");
|
||||
$ldap_urls[] = [ $ldap_url, true, true ];
|
||||
$pretty_ldap_urls[] = [ $ldap_url, "YES", "YES" ];
|
||||
$pretty_ldap_urls[] = [$ldap_url, "enabled", "STARTTLS ENABLED"];
|
||||
continue;
|
||||
} else {
|
||||
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without STARTTLS");
|
||||
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without certificate checks.");
|
||||
}
|
||||
|
||||
if ($this->test_anonymous_bind($ldap_url, false, true)) {
|
||||
$this->info("Plain connection to $ldap_url with STARTTLS and cert checks *disabled* successful!");
|
||||
$ldap_urls[] = [$ldap_url, false, true];
|
||||
$pretty_ldap_urls[] = [$ldap_url, "DISABLED", "STARTTLS ENABLED"];
|
||||
continue;
|
||||
} else {
|
||||
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled, and cert checks disabled. Trying without STARTTLS");
|
||||
}
|
||||
|
||||
if($this->test_anonymous_bind($ldap_url)) {
|
||||
$this->info("Plain connection to $ldap_url succesful!");
|
||||
$ldap_urls[] = [ $ldap_url, true, false ];
|
||||
$pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ];
|
||||
$pretty_ldap_urls[] = [$ldap_url, "n/a", "starttls disabled"];
|
||||
continue;
|
||||
} else {
|
||||
$this->error("WARNING: Failed to bind to $ldap_url. Giving up on port $port");
|
||||
@@ -313,23 +358,29 @@ class LdapTroubleshooter extends Command
|
||||
$this->debugout(print_r($ldap_urls,true));
|
||||
|
||||
if(count($ldap_urls) > 0 ) {
|
||||
$this->info("Found working LDAP URL's: ");
|
||||
$this->debugout("Found working LDAP URL's: ");
|
||||
foreach($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead?
|
||||
$this->info("LDAP URL: ".$ldap_url[0]);
|
||||
$this->info($ldap_url[0]. ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled"). ($ldap_url[2] ? " STARTTLS Enabled ": " STARTTLS Disabled"));
|
||||
$this->debugout("LDAP URL: " . $ldap_url[0]);
|
||||
$this->debugout($ldap_url[0] . ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled") . ($ldap_url[2] ? " STARTTLS Enabled " : " STARTTLS Disabled"));
|
||||
}
|
||||
$this->table(["URL", "Cert Checks Enabled?", "STARTTLS Enabled?"],$pretty_ldap_urls);
|
||||
$this->table(["URL", "Cert Checks?", "STARTTLS?"], $pretty_ldap_urls);
|
||||
} else {
|
||||
$this->error("ERROR - no valid LDAP URL's available - ABORTING");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$this->info("STAGE 4: Test Administrative Bind for LDAP Sync");
|
||||
$this->line("STAGE 4: Test Administrative Bind for LDAP Sync");
|
||||
foreach($ldap_urls AS $ldap_url) {
|
||||
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, Crypt::decrypt($settings->ldap_pword));
|
||||
try {
|
||||
$w = Crypt::Decrypt($settings->ldap_pword);
|
||||
} catch (\Exception $e) {
|
||||
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
|
||||
exit(0);
|
||||
}
|
||||
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, $w);
|
||||
}
|
||||
|
||||
$this->info("STAGE 5: Test BaseDN");
|
||||
$this->line("STAGE 5: Test BaseDN");
|
||||
//grab all LDAP_ constants and fill up a reversed array mapping from weird LDAP dotted-strings to (Constant Name)
|
||||
$all_defined_constants = get_defined_constants();
|
||||
$ldap_constants = [];
|
||||
@@ -341,16 +392,23 @@ class LdapTroubleshooter extends Command
|
||||
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
|
||||
|
||||
foreach($ldap_urls AS $ldap_url) {
|
||||
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,Crypt::decrypt($settings->ldap_pword),$settings)) {
|
||||
try {
|
||||
$w = Crypt::Decrypt($settings->ldap_pword);
|
||||
} catch (\Exception $e) {
|
||||
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,$w,$settings)) {
|
||||
$this->info("Success getting informational bind!");
|
||||
} else {
|
||||
$this->error("Unable to get information from bind.");
|
||||
}
|
||||
}
|
||||
|
||||
$this->info("STAGE 6: Test LDAP Login to Snipe-IT");
|
||||
$this->line("STAGE 6: Test LDAP Login to Snipe-IT");
|
||||
foreach($ldap_urls AS $ldap_url) {
|
||||
$this->info("Starting auth to ".$ldap_url[0]);
|
||||
$this->line("Starting auth to " . $ldap_url[0]);
|
||||
while(true) {
|
||||
$with_tls = $ldap_url[1] ? "with": "without";
|
||||
$with_startssl = $ldap_url[2] ? "using": "not using";
|
||||
@@ -359,7 +417,12 @@ class LdapTroubleshooter extends Command
|
||||
}
|
||||
$username = $this->ask("Username");
|
||||
$password = $this->secret("Password");
|
||||
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results?
|
||||
$results = $this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results?
|
||||
if ($results) {
|
||||
$this->info("Success authenticating with " . $username);
|
||||
} else {
|
||||
$this->error("Unable to authenticate with " . $username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,14 +431,17 @@ class LdapTroubleshooter extends Command
|
||||
|
||||
public function connect_to_ldap($ldap_url, $check_cert, $start_tls)
|
||||
{
|
||||
if ($check_cert) {
|
||||
$this->line("we *ARE* checking certs");
|
||||
Ldap::ignoreCertificates(false);
|
||||
|
||||
} else {
|
||||
$this->line("we are IGNORING certs");
|
||||
Ldap::ignoreCertificates(true);
|
||||
}
|
||||
$lconn = ldap_connect($ldap_url);
|
||||
ldap_set_option($lconn, LDAP_OPT_PROTOCOL_VERSION, 3); // should we 'test' different protocol versions here? Does anyone even use anything other than LDAPv3?
|
||||
// no - it's formally deprecated: https://tools.ietf.org/html/rfc3494
|
||||
if(!$check_cert) {
|
||||
putenv('LDAPTLS_REQCERT=never'); // This is horrible; is this *really* the only way to do it?
|
||||
} else {
|
||||
putenv('LDAPTLS_REQCERT'); // have to very explicitly and manually *UN* set the env var here to ensure it works
|
||||
}
|
||||
if($this->settings->ldap_client_tls_cert && $this->settings->ldap_client_tls_key) {
|
||||
// client-side TLS certificate support for LDAP (Google Secure LDAP)
|
||||
putenv('LDAPTLS_CERT=storage/ldap_client_tls.cert');
|
||||
@@ -404,9 +470,10 @@ class LdapTroubleshooter extends Command
|
||||
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert , $start_tls) {
|
||||
try {
|
||||
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
|
||||
$this->info("gonna try to bind now, this can take a while if we mess it up");
|
||||
$this->line("Attempting to bind now, this can take a while if we mess it up");
|
||||
$bind_results = ldap_bind($lconn);
|
||||
$this->info("Bind results are: ".$bind_results." which translate into boolean: ".(bool)$bind_results);
|
||||
$this->line("Bind results are: " . $bind_results . " which translate into boolean: " . (bool)$bind_results);
|
||||
ldap_close($lconn);
|
||||
return (bool)$bind_results;
|
||||
} catch (Exception $e) {
|
||||
$this->error("WARNING: Exception caught during bind - ".$e->getMessage());
|
||||
@@ -421,6 +488,7 @@ class LdapTroubleshooter extends Command
|
||||
try {
|
||||
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
|
||||
$bind_results = ldap_bind($lconn, $username, $password);
|
||||
ldap_close($lconn);
|
||||
if(!$bind_results) {
|
||||
$this->error("WARNING: Failed to bind to $ldap_url as $username");
|
||||
return false;
|
||||
@@ -446,22 +514,62 @@ class LdapTroubleshooter extends Command
|
||||
return false;
|
||||
}
|
||||
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
|
||||
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
|
||||
$results = ldap_get_entries($conn, $result);
|
||||
$cleaned_results = $this->ldap_results_cleaner($results);
|
||||
$this->line(print_r($cleaned_results,true));
|
||||
//okay, great - now how do we display those results? I have no idea.
|
||||
$cleaned_results = [];
|
||||
try {
|
||||
// This _may_ only work for Active Directory?
|
||||
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
|
||||
$results = ldap_get_entries($conn, $result);
|
||||
$cleaned_results = $this->ldap_results_cleaner($results);
|
||||
//$this->line(print_r($cleaned_results,true));
|
||||
$default_naming_contexts = $cleaned_results[0]['namingcontexts'];
|
||||
$this->info("Default Naming Contexts:");
|
||||
$this->info(implode(", ", $default_naming_contexts));
|
||||
//okay, great - now how do we display those results? I have no idea.
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Unable to get base naming contexts - here's what we *did* get:");
|
||||
$this->line(print_r($cleaned_results, true));
|
||||
}
|
||||
// I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it?
|
||||
$this->comment("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
|
||||
$this->debugout("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
|
||||
$this->debugout("Base DN is: ".$settings->ldap_basedn." and filter is: ".parenthesized_filter($settings->ldap_filter));
|
||||
$search_results = ldap_search($conn, $settings->ldap_basedn, parenthesized_filter($settings->ldap_filter));
|
||||
$entries = ldap_get_entries($conn, $search_results);
|
||||
$this->info("Printing first 10 results: ");
|
||||
for($i=0;$i<10;$i++) {
|
||||
$this->info($search_results[$i]);
|
||||
$pretty_data = array_slice($this->ldap_results_cleaner($entries), 0, 10);
|
||||
//print_r($data);
|
||||
$headers = [];
|
||||
foreach ($pretty_data as $row) {
|
||||
//populate headers
|
||||
foreach ($row as $key => $value) {
|
||||
//skip objectsid and objectguid because it junks up output
|
||||
if ($key == "objectsid" || $key == "objectguid") {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($key, $headers)) {
|
||||
$headers[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
$table = [];
|
||||
//repeat again to populate table
|
||||
foreach ($pretty_data as $row) {
|
||||
$newrow = [];
|
||||
foreach ($headers as $header) {
|
||||
if (is_array(@$row[$header])) {
|
||||
$newrow[] = "[" . implode(", ", $row[$header]) . "]";
|
||||
} else {
|
||||
$newrow[] = @$row[$header];
|
||||
}
|
||||
}
|
||||
$table[] = $newrow;
|
||||
}
|
||||
|
||||
$this->table($headers, $table);
|
||||
} catch (\Exception $e) {
|
||||
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
|
||||
return false;
|
||||
} finally {
|
||||
ldap_close($conn);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -477,7 +585,7 @@ class LdapTroubleshooter extends Command
|
||||
{
|
||||
if(!(function_exists('pcntl_sigtimedwait') && function_exists('posix_getpid') && function_exists('pcntl_fork') && function_exists('posix_kill') && function_exists('pcntl_wifsignaled'))) {
|
||||
// POSIX functions needed for forking aren't present, just run the function inline (ignoring timeout)
|
||||
$this->info('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
|
||||
$this->line('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
|
||||
return $function();
|
||||
} else {
|
||||
$parent_pid = posix_getpid();
|
||||
@@ -514,4 +622,6 @@ class LdapTroubleshooter extends Command
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ class MoveUploadsToNewDisk extends Command
|
||||
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
|
||||
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
|
||||
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
|
||||
$private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'."/*.*");
|
||||
$private_uploads['assetmodels'] = glob('storage/private_uploads/models'."/*.*");
|
||||
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
|
||||
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
|
||||
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");
|
||||
|
||||
@@ -8,8 +8,6 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Console\Helper\ProgressIndicator;
|
||||
|
||||
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); //600 seconds = 10 minutes
|
||||
ini_set('memory_limit', env('IMPORT_MEMORY_LIMIT', '500M'));
|
||||
|
||||
/**
|
||||
* Class ObjectImportCommand
|
||||
@@ -52,6 +50,9 @@ class ObjectImportCommand extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); //600 seconds = 10 minutes
|
||||
ini_set('memory_limit', env('IMPORT_MEMORY_LIMIT', '500M'));
|
||||
|
||||
$this->progressIndicator = new ProgressIndicator($this->output);
|
||||
|
||||
$filename = $this->argument('filename');
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\CustomField;
|
||||
use Schema;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
@@ -59,6 +59,9 @@ class PaveIt extends Command
|
||||
'migrations',
|
||||
'settings',
|
||||
'users',
|
||||
'telescope_entries',
|
||||
'telescope_entries_tags',
|
||||
'telescope_monitoring',
|
||||
];
|
||||
|
||||
// We only need to find out what these are so we can nuke these columns on the assets table.
|
||||
@@ -66,8 +69,8 @@ class PaveIt extends Command
|
||||
foreach ($custom_fields as $custom_field) {
|
||||
$this->info('DROP the '.$custom_field->db_column.' column from assets as well.');
|
||||
|
||||
if (\Schema::hasColumn('assets', $custom_field->db_column)) {
|
||||
\Schema::table('assets', function ($table) use ($custom_field) {
|
||||
if (Schema::hasColumn('assets', $custom_field->db_column)) {
|
||||
Schema::table('assets', function ($table) use ($custom_field) {
|
||||
$table->dropColumn($custom_field->db_column);
|
||||
});
|
||||
}
|
||||
@@ -84,8 +87,8 @@ class PaveIt extends Command
|
||||
}
|
||||
|
||||
// Leave in the demo oauth keys so we don't have to reset them every day in the demos
|
||||
\DB::statement('delete from oauth_clients WHERE id > 2');
|
||||
\DB::statement('delete from oauth_access_tokens WHERE id > 2');
|
||||
DB::statement('delete from oauth_clients WHERE id > 2');
|
||||
DB::statement('delete from oauth_access_tokens WHERE user_id > 2');
|
||||
|
||||
}
|
||||
}
|
||||
@@ -62,19 +62,19 @@ class Purge extends Command
|
||||
$assetcount = $assets->count();
|
||||
$this->info($assets->count().' assets purged.');
|
||||
$asset_assoc = 0;
|
||||
$asset_maintenances = 0;
|
||||
$maintenances = 0;
|
||||
|
||||
foreach ($assets as $asset) {
|
||||
$this->info('- Asset "'.$asset->present()->name().'" deleted.');
|
||||
$this->info('- Asset "'.$asset->display_name.'" deleted.');
|
||||
$asset_assoc += $asset->assetlog()->count();
|
||||
$asset->assetlog()->forceDelete();
|
||||
$asset_maintenances += $asset->assetmaintenances()->count();
|
||||
$asset->assetmaintenances()->forceDelete();
|
||||
$maintenances += $asset->maintenances()->count();
|
||||
$asset->maintenances()->forceDelete();
|
||||
$asset->forceDelete();
|
||||
}
|
||||
|
||||
$this->info($asset_assoc.' corresponding log records purged.');
|
||||
$this->info($asset_maintenances.' corresponding maintenance records purged.');
|
||||
$this->info($maintenances.' corresponding maintenance records purged.');
|
||||
|
||||
$locations = Location::whereNotNull('deleted_at')->withTrashed()->get();
|
||||
$this->info($locations->count().' locations purged.');
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Actionlog;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RemoveInvalidUploadDeleteActionLogItems extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:remove-invalid-upload-delete-action-log-items';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Permanently remove invalid "upload deleted" action log items that have a null filename. This command can potentially result in deleted files being "resurrected" in the UI.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$invalidLogs = Actionlog::query()
|
||||
->where('action_type', 'upload deleted')
|
||||
->whereNull('filename')
|
||||
->withTrashed()
|
||||
->get();
|
||||
|
||||
$this->info("{$invalidLogs->count()} invalid log items found.");
|
||||
|
||||
if ($invalidLogs->count() === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->table(['ID', 'Action Type', 'Item Type', 'Item ID', 'Created At', 'Deleted At'], $invalidLogs->map(fn($log) => [
|
||||
$log->id,
|
||||
$log->action_type,
|
||||
$log->item_type,
|
||||
$log->item_id,
|
||||
$log->created_at,
|
||||
$log->deleted_at,
|
||||
])->toArray());
|
||||
|
||||
if ($this->confirm("Do you wish to remove {$invalidLogs->count()} log items?")) {
|
||||
$invalidLogs->each(fn($log) => $log->forceDelete());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Console\Commands;
|
||||
use Illuminate\Console\Command;
|
||||
use ZipArchive;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use enshrined\svgSanitize\Sanitizer;
|
||||
|
||||
class SQLStreamer {
|
||||
private $input;
|
||||
@@ -242,7 +243,10 @@ class RestoreFromBackup extends Command
|
||||
|
||||
$private_dirs = [
|
||||
'storage/private_uploads/accessories',
|
||||
'storage/private_uploads/assetmodels',
|
||||
'storage/private_uploads/assetmodels' => 'storage/private_uploads/models', //this was changed from assetmodels => models Aug 10 2025
|
||||
'storage/private_uploads/asset_maintenances' => 'storage/private_uploads/maintenances', //this was changed from asset_maintenances => maintenances Aug 10 2025
|
||||
'storage/private_uploads/maintenances', //but let 'maintenances' take precedence
|
||||
'storage/private_uploads/models', //and let 'models' take precedence
|
||||
'storage/private_uploads/assets', // these are asset _files_, not the pictures.
|
||||
'storage/private_uploads/audits',
|
||||
'storage/private_uploads/components',
|
||||
@@ -260,9 +264,10 @@ class RestoreFromBackup extends Command
|
||||
];
|
||||
$public_dirs = [
|
||||
'public/uploads/accessories',
|
||||
// 'public/uploads/assetmodels' => 'public/uploads/models', //according to git, this was _never_ a thing... (see below)
|
||||
'public/uploads/maintenances',
|
||||
'public/uploads/assets', // these are asset _pictures_, not asset files
|
||||
'public/uploads/avatars',
|
||||
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
|
||||
'public/uploads/categories',
|
||||
'public/uploads/companies',
|
||||
'public/uploads/components',
|
||||
@@ -270,7 +275,7 @@ class RestoreFromBackup extends Command
|
||||
'public/uploads/departments',
|
||||
'public/uploads/locations',
|
||||
'public/uploads/manufacturers',
|
||||
'public/uploads/models',
|
||||
'public/uploads/models', // ...it's been this way for 9 years (as of late 2025)
|
||||
'public/uploads/suppliers',
|
||||
];
|
||||
|
||||
@@ -283,8 +288,6 @@ class RestoreFromBackup extends Command
|
||||
'public/uploads/favicon-uploaded.*',
|
||||
];
|
||||
|
||||
$all_files = $private_dirs + $public_dirs;
|
||||
|
||||
$sqlfiles = [];
|
||||
$sqlfile_indices = [];
|
||||
|
||||
@@ -292,6 +295,20 @@ class RestoreFromBackup extends Command
|
||||
$boring_files = [];
|
||||
$unsafe_files = [];
|
||||
|
||||
$good_extensions = config('filesystems.allowed_upload_extensions_array');
|
||||
|
||||
$private_extensions = array_merge($good_extensions, ["csv", "key"]); //add csv, and 'key'
|
||||
$public_extensions = array_diff($good_extensions, ["xml"]); //remove xml
|
||||
|
||||
$sanitizer = new Sanitizer();
|
||||
|
||||
/**
|
||||
* TODO: I _hate_ the "continue 3" thing we keep doing here
|
||||
* I think a better approach might be to have the "each file" stuff be in a method on this class, and the
|
||||
* boring_files and interesting_files be properties on it that we fill out. Then, in that method, we could
|
||||
* just do a 'return' once the file is actually handled (yay or nay). We could also start to break out some of
|
||||
* the _other_ things that we do into their own methods too? But I don't care about that as much.
|
||||
*/
|
||||
for ($i = 0; $i < $za->numFiles; $i++) {
|
||||
$stat_results = $za->statIndex($i);
|
||||
// echo "index: $i\n";
|
||||
@@ -306,7 +323,7 @@ class RestoreFromBackup extends Command
|
||||
// skip macOS resource fork files (?!?!?!)
|
||||
if (strpos($raw_path, '__MACOSX') !== false && strpos($raw_path, '._') !== false) {
|
||||
//print "SKIPPING macOS Resource fork file: $raw_path\n";
|
||||
$boring_files[] = $raw_path;
|
||||
// $boring_files[] = $raw_path; //stop adding this to the boring files list; it's just confusing
|
||||
continue;
|
||||
}
|
||||
if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') {
|
||||
@@ -315,44 +332,70 @@ class RestoreFromBackup extends Command
|
||||
$sqlfile_indices[] = $i;
|
||||
continue;
|
||||
}
|
||||
if ($raw_path[-1] == '/') {
|
||||
//last character is '/' - this is a directory, and we don't need it, and we don't need to warn about it
|
||||
continue;
|
||||
}
|
||||
if (in_array(basename($raw_path), [".gitkeep", ".gitignore", ".DS_Store"])) {
|
||||
//skip these boring files silently without reporting on them; they're stupid
|
||||
continue;
|
||||
}
|
||||
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
|
||||
|
||||
foreach (array_merge($private_dirs, $public_dirs) as $dir) {
|
||||
$last_pos = strrpos($raw_path, $dir . '/');
|
||||
if ($last_pos !== false) {
|
||||
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
|
||||
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
|
||||
$interesting_files[$raw_path] = ['dest' => $dir, 'index' => $i];
|
||||
continue 2;
|
||||
if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) {
|
||||
// we don't care about that; we just want files with the appropriate prefix
|
||||
//print("FOUND THE EXACT DIRECTORY: $dir AT: $raw_path!!!\n");
|
||||
foreach (['public' => $public_dirs, 'private' => $private_dirs] as $purpose => $dirs) {
|
||||
$allowed_extensions = match ($purpose) {
|
||||
'public' => $public_extensions,
|
||||
'private' => $private_extensions,
|
||||
};
|
||||
foreach ($dirs as $dir => $destdir) {
|
||||
if (is_int($dir)) {
|
||||
$dir = $destdir;
|
||||
}
|
||||
$last_pos = strrpos($raw_path, $dir . '/');
|
||||
if ($last_pos !== false) {
|
||||
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
|
||||
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
|
||||
//the CSV bit, below, is because we store CSV files as "blahcsv" - without an extension
|
||||
if (!in_array($extension, $allowed_extensions) && !($dir == "storage/private_uploads/imports" && substr($raw_path, -3) == "csv" && $extension == "")) {
|
||||
$unsafe_files[] = $raw_path;
|
||||
Log::debug($raw_path . ' from directory ' . $dir . ' is being skipped');
|
||||
} else {
|
||||
if ($dir != $destdir) {
|
||||
Log::debug("Getting ready to save file $raw_path to new directory $destdir");
|
||||
}
|
||||
$interesting_files[$raw_path] = ['dest' => $destdir, 'index' => $i];
|
||||
}
|
||||
continue 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
$good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
|
||||
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico', 'avif'
|
||||
];
|
||||
foreach (array_merge($private_files, $public_files) as $file) {
|
||||
$has_wildcard = (strpos($file, '*') !== false);
|
||||
if ($has_wildcard) {
|
||||
$file = substr($file, 0, -1); //trim last character (which should be the wildcard)
|
||||
}
|
||||
$last_pos = strrpos($raw_path, $file); // no trailing slash!
|
||||
if ($last_pos !== false) {
|
||||
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, $good_extensions)) {
|
||||
// gathering potentially unsafe files here to return at exit
|
||||
$unsafe_files[] = $raw_path;
|
||||
Log::debug('Potentially unsafe file '.$raw_path.' is being skipped');
|
||||
$boring_files[] = $raw_path;
|
||||
continue 2;
|
||||
|
||||
foreach (['public' => $public_files, 'private' => $private_files] as $purpose => $files) {
|
||||
$allowed_extensions = match ($purpose) {
|
||||
'public' => $public_extensions,
|
||||
'private' => $private_extensions,
|
||||
};
|
||||
foreach ($files as $file) {
|
||||
$has_wildcard = (strpos($file, '*') !== false);
|
||||
if ($has_wildcard) {
|
||||
$file = substr($file, 0, -1); //trim last character (which should be the wildcard)
|
||||
}
|
||||
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $file - last_pos+strlen(\$file) is: ".($last_pos+strlen($file))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
|
||||
//no wildcards found in $file, process 'normally'
|
||||
if ($last_pos + strlen($file) == strlen($raw_path) || $has_wildcard) { //again, no trailing slash. or this is a wildcard and we just take it.
|
||||
// print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
|
||||
$interesting_files[$raw_path] = ['dest' => dirname($file), 'index' => $i];
|
||||
continue 2;
|
||||
$last_pos = strrpos($raw_path, $file); // no trailing slash!
|
||||
if ($last_pos !== false) {
|
||||
if (!in_array($extension, $allowed_extensions)) {
|
||||
// gathering potentially unsafe files here to return at exit
|
||||
$unsafe_files[] = $raw_path;
|
||||
Log::debug('Potentially unsafe file ' . $raw_path . ' is being skipped');
|
||||
$boring_files[] = $raw_path;
|
||||
continue 3;
|
||||
}
|
||||
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $file - last_pos+strlen(\$file) is: ".($last_pos+strlen($file))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
|
||||
//no wildcards found in $file, process 'normally'
|
||||
if ($last_pos + strlen($file) == strlen($raw_path) || $has_wildcard) { //again, no trailing slash. or this is a wildcard and we just take it.
|
||||
// print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
|
||||
$interesting_files[$raw_path] = ['dest' => dirname($file), 'index' => $i];
|
||||
continue 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -489,18 +532,25 @@ class RestoreFromBackup extends Command
|
||||
}
|
||||
foreach ($interesting_files as $pretty_file_name => $file_details) {
|
||||
$ugly_file_name = $za->statIndex($file_details['index'])['name'];
|
||||
$fp = $za->getStream($ugly_file_name);
|
||||
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
|
||||
if (!is_dir($file_details['dest'])) {
|
||||
mkdir($file_details['dest'], 0755, true); //0755 is what Laravel uses, so we do that
|
||||
$migrated_file_name = $file_details['dest'] . '/' . basename($pretty_file_name);
|
||||
if (strcasecmp(substr($pretty_file_name, -4), ".svg") === 0) {
|
||||
$svg_contents = $za->getFromIndex($file_details['index']);
|
||||
$cleaned_svg = $sanitizer->sanitize($svg_contents);
|
||||
file_put_contents($migrated_file_name, $cleaned_svg);
|
||||
} else {
|
||||
$fp = $za->getStream($ugly_file_name);
|
||||
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
|
||||
if (!is_dir($file_details['dest'])) {
|
||||
mkdir($file_details['dest'], 0755, true); //0755 is what Laravel uses, so we do that
|
||||
}
|
||||
$migrated_file = fopen($migrated_file_name, 'w');
|
||||
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
|
||||
fwrite($migrated_file, $buffer);
|
||||
}
|
||||
fclose($migrated_file);
|
||||
fclose($fp);
|
||||
//$this->info("Wrote $ugly_file_name to $pretty_file_name");
|
||||
}
|
||||
$migrated_file = fopen($file_details['dest'].'/'.basename($pretty_file_name), 'w');
|
||||
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
|
||||
fwrite($migrated_file, $buffer);
|
||||
}
|
||||
fclose($migrated_file);
|
||||
fclose($fp);
|
||||
//$this->info("Wrote $ugly_file_name to $pretty_file_name");
|
||||
if ($bar) {
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class SendAcceptanceReminder extends Command
|
||||
if(!$email){
|
||||
$no_email_list[] = [
|
||||
'id' => $acceptance->assignedTo?->id,
|
||||
'name' => $acceptance->assignedTo?->present()->fullName(),
|
||||
'name' => $acceptance->assignedTo?->display_name,
|
||||
];
|
||||
} else {
|
||||
$count++;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Mail\ExpiringAssetsMail;
|
||||
use App\Mail\ExpiringLicenseMail;
|
||||
use App\Models\Asset;
|
||||
@@ -52,19 +53,73 @@ class SendExpirationAlerts extends Command
|
||||
->filter(fn($item) => !empty($item))
|
||||
->all();
|
||||
// Expiring Assets
|
||||
$assets = Asset::getExpiringWarrantee($alert_interval);
|
||||
$assets = Asset::getExpiringWarrantyOrEol($alert_interval);
|
||||
|
||||
if ($assets->count() > 0) {
|
||||
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
|
||||
|
||||
Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval));
|
||||
|
||||
$this->table(
|
||||
[
|
||||
trans('general.id'),
|
||||
trans('admin/hardware/form.tag'),
|
||||
trans('admin/hardware/form.model'),
|
||||
trans('general.model_no'),
|
||||
trans('general.purchase_date'),
|
||||
trans('admin/hardware/form.eol_rate'),
|
||||
trans('admin/hardware/form.eol_date'),
|
||||
trans('admin/hardware/form.warranty_expires'),
|
||||
],
|
||||
$assets->map(fn($item) =>
|
||||
[
|
||||
trans('general.id') => $item->id,
|
||||
trans('admin/hardware/form.tag') => $item->asset_tag,
|
||||
trans('admin/hardware/form.model') => $item->model->name,
|
||||
trans('general.model_no') => $item->model->model_number,
|
||||
trans('general.purchase_date') => $item->purchase_date_formatted,
|
||||
trans('admin/hardware/form.eol_rate') => $item->model->eol,
|
||||
trans('admin/hardware/form.eol_date') => $item->eol_date ? $item->eol_formatted_date .' ('.$item->eol_diff_for_humans.')' : '',
|
||||
trans('admin/hardware/form.warranty_expires') => $item->warranty_expires ? $item->warranty_expires_formatted_date .' ('.$item->warranty_expires_diff_for_humans.')' : '',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// Expiring licenses
|
||||
$licenses = License::getExpiringLicenses($alert_interval);
|
||||
$licenses = License::query()->ExpiringLicenses($alert_interval)
|
||||
->with('manufacturer','category')
|
||||
->orderBy('expiration_date', 'ASC')
|
||||
->orderBy('termination_date', 'ASC')
|
||||
->get();
|
||||
if ($licenses->count() > 0) {
|
||||
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
|
||||
Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $alert_interval));
|
||||
|
||||
$this->table(
|
||||
[
|
||||
trans('general.id'),
|
||||
trans('general.name'),
|
||||
trans('general.purchase_date'),
|
||||
trans('admin/licenses/form.expiration'),
|
||||
trans('mail.expires'),
|
||||
trans('admin/licenses/form.termination_date'),
|
||||
trans('mail.terminates')],
|
||||
$licenses->map(fn($item) => [
|
||||
trans('general.id') => $item->id,
|
||||
trans('general.name') => $item->name,
|
||||
trans('general.purchase_date') => $item->purchase_date_formatted,
|
||||
trans('admin/licenses/form.expiration') => $item->expires_formatted_date,
|
||||
trans('mail.expires') => $item->expires_formatted_date ? $item->expires_diff_for_humans : '',
|
||||
trans('admin/licenses/form.termination_date') => $item->terminates_formatted_date,
|
||||
trans('mail.terminates') => $item->terminates_diff_for_humans
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
// Send a message even if the count is 0
|
||||
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
|
||||
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
if ($settings->alert_email == '') {
|
||||
$this->error('Could not send email. No alert email configured in settings');
|
||||
|
||||
@@ -16,7 +16,7 @@ class SendUpcomingAuditReport extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:upcoming-audits';
|
||||
protected $signature = 'snipeit:upcoming-audits {--with-output : Display the results in a table in your console in addition to sending the email}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -47,21 +47,69 @@ class SendUpcomingAuditReport extends Command
|
||||
$today = Carbon::now();
|
||||
$interval_date = $today->copy()->addDays($interval);
|
||||
|
||||
$assets = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get();
|
||||
$this->info($assets->count() . ' assets must be audited in on or before ' . $interval_date . ' is deadline');
|
||||
|
||||
|
||||
if ((count($assets) !== 0) && ($assets->count() > 0) && ($settings->alert_email != '')) {
|
||||
// Send a rollup to the admin, if settings dictate
|
||||
$recipients = collect(explode(',', $settings->alert_email))
|
||||
->map(fn($item) => trim($item))
|
||||
->filter(fn($item) => !empty($item))
|
||||
->all();
|
||||
|
||||
|
||||
$this->info('Sending Admin SendUpcomingAuditNotification to: ' . $settings->alert_email);
|
||||
Mail::to($recipients)->send(new SendUpcomingAuditMail($assets, $settings->audit_warning_days));
|
||||
$assets_query = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'asc')->with('supplier');
|
||||
$asset_count = $assets_query->count();
|
||||
$this->info(number_format($asset_count) . ' assets must be audited on or before ' . $interval_date);
|
||||
if (!$this->option('with-output')) {
|
||||
$this->info('Run this command with the --with-output option to see the full list in the console.');
|
||||
}
|
||||
|
||||
|
||||
if ($asset_count > 0) {
|
||||
|
||||
$assets_for_email = $assets_query->limit(30)->get();
|
||||
|
||||
// Send a rollup to the admin, if settings dictate
|
||||
if ($settings->alert_email != '') {
|
||||
|
||||
$recipients = collect(explode(',', $settings->alert_email))
|
||||
->map(fn($item) => trim($item))
|
||||
->filter(fn($item) => !empty($item))
|
||||
->all();
|
||||
|
||||
Mail::to($recipients)->send(new SendUpcomingAuditMail($assets_for_email, $settings->audit_warning_days, $asset_count));
|
||||
$this->info('Audit notification sent to: ' . $settings->alert_email);
|
||||
|
||||
} else {
|
||||
$this->info('There is no admin alert email set so no email will be sent.');
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ($this->option('with-output')) {
|
||||
|
||||
|
||||
// Get the full list if the user wants output in the console
|
||||
$assets_for_output = $assets_query->limit(null)->get();
|
||||
|
||||
$this->table(
|
||||
[
|
||||
trans('general.id'),
|
||||
trans('general.name'),
|
||||
trans('general.last_audit'),
|
||||
trans('general.next_audit_date'),
|
||||
trans('mail.Days'),
|
||||
trans('mail.supplier'),
|
||||
trans('mail.assigned_to'),
|
||||
|
||||
],
|
||||
$assets_for_output->map(fn($item) => [
|
||||
trans('general.id') => $item->id,
|
||||
trans('general.name') => $item->display_name,
|
||||
trans('general.last_audit') => $item->last_audit_formatted_date,
|
||||
trans('general.next_audit_date') => $item->next_audit_formatted_date,
|
||||
trans('mail.Days') => round($item->next_audit_diff_in_days),
|
||||
trans('mail.supplier') => $item->supplier ? $item->supplier->name : '',
|
||||
trans('mail.assigned_to') => $item->assignedTo ? $item->assignedTo->display_name : '',
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->info('There are no assets due for audit in the next ' . $interval . ' days.');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ class SystemBackup extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
ini_set('max_execution_time', env('BACKUP_TIME_LIMIT', 600)); //600 seconds = 10 minutes
|
||||
|
||||
if ($this->option('filename')) {
|
||||
$filename = $this->option('filename');
|
||||
|
||||
|
||||
@@ -116,18 +116,35 @@ class Handler extends ExceptionHandler
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
|
||||
default:
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// This handles API validation exceptions that happen at the Form Request level, so they
|
||||
// never even get to the controller where we normally nicely format JSON responses
|
||||
if ($e instanceof ValidationException) {
|
||||
$response = $this->invalidJson($request, $e);
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $e->errors()), 200);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// This is traaaaash but it handles models that are not found while using route model binding :(
|
||||
// The only alternative is to set that at *each* route, which is crazypants
|
||||
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
|
||||
$ids = method_exists($e, 'getIds') ? $e->getIds() : [];
|
||||
|
||||
if (in_array('bulkedit', $ids, true)) {
|
||||
$error_array = session()->get('bulk_asset_errors');
|
||||
return redirect()
|
||||
->route('hardware.index')
|
||||
->withErrors($error_array, 'bulk_asset_errors')
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// This gets the MVC model name from the exception and formats in a way that's less fugly
|
||||
$model_name = strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel())))));
|
||||
$model_name = trim(strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel()))))));
|
||||
$route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
|
||||
|
||||
// Sigh.
|
||||
@@ -143,9 +160,7 @@ class Handler extends ExceptionHandler
|
||||
$route = 'maintenances.index';
|
||||
} elseif ($route === 'licenseseats.index') {
|
||||
$route = 'licenses.index';
|
||||
} elseif ($route === 'customfields.index') {
|
||||
$route = 'fields.index';
|
||||
} elseif ($route === 'customfieldsets.index') {
|
||||
} elseif (($route === 'customfieldsets.index') || ($route === 'customfields.index')) {
|
||||
$route = 'fields.index';
|
||||
}
|
||||
|
||||
|
||||
10
app/Exceptions/ItemStillHasAccessories.php
Normal file
10
app/Exceptions/ItemStillHasAccessories.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasAccessories extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasAssetModels.php
Normal file
10
app/Exceptions/ItemStillHasAssetModels.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasAssetModels extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
9
app/Exceptions/ItemStillHasAssets.php
Normal file
9
app/Exceptions/ItemStillHasAssets.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasAssets extends ItemStillHasChildren
|
||||
{
|
||||
}
|
||||
14
app/Exceptions/ItemStillHasChildren.php
Normal file
14
app/Exceptions/ItemStillHasChildren.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasChildren extends Exception
|
||||
{
|
||||
//public function __construct($message, $code = 0, Exception $previous = null, $parent, $children)
|
||||
//{
|
||||
// trans()
|
||||
//
|
||||
//}
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasComponents.php
Normal file
10
app/Exceptions/ItemStillHasComponents.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasComponents extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasConsumables.php
Normal file
10
app/Exceptions/ItemStillHasConsumables.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasConsumables extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasLicenses.php
Normal file
10
app/Exceptions/ItemStillHasLicenses.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasLicenses extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
10
app/Exceptions/ItemStillHasMaintenances.php
Normal file
10
app/Exceptions/ItemStillHasMaintenances.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ItemStillHasMaintenances extends ItemStillHasChildren
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use App\Models\Setting;
|
||||
use App\Models\Statuslabel;
|
||||
use App\Models\License;
|
||||
use App\Models\Location;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Carbon\Carbon;
|
||||
@@ -94,7 +95,7 @@ class Helper
|
||||
$Parsedown->setSafeMode(true);
|
||||
|
||||
if ($str) {
|
||||
return $Parsedown->text($str);
|
||||
return $Parsedown->text(strip_tags($str));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +105,7 @@ class Helper
|
||||
$Parsedown->setSafeMode(true);
|
||||
|
||||
if ($str) {
|
||||
return $Parsedown->line($str);
|
||||
return $Parsedown->line(strip_tags($str));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,6 +435,34 @@ class Helper
|
||||
return $colors[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string has any RTL characters
|
||||
* @param $value
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasRtl($string) {
|
||||
$rtlChar = '/[\x{0590}-\x{083F}]|[\x{08A0}-\x{08FF}]|[\x{FB1D}-\x{FDFF}]|[\x{FE70}-\x{FEFF}]/u';
|
||||
return preg_match($rtlChar, $string) != 0;
|
||||
}
|
||||
|
||||
// is chinese, japanese or korean language
|
||||
public static function isCjk($string) {
|
||||
return Helper::isChinese($string) || Helper::isJapanese($string) || Helper::isKorean($string);
|
||||
}
|
||||
|
||||
public static function isChinese($string) {
|
||||
return preg_match("/\p{Han}+/u", $string);
|
||||
}
|
||||
|
||||
public static function isJapanese($string) {
|
||||
return preg_match('/[\x{4E00}-\x{9FBF}\x{3040}-\x{309F}\x{30A0}-\x{30FF}]/u', $string);
|
||||
}
|
||||
|
||||
public static function isKorean($string) {
|
||||
return preg_match('/[\x{3130}-\x{318F}\x{AC00}-\x{D7AF}]/u', $string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Increases or decreases the brightness of a color by a percentage of the current brightness.
|
||||
*
|
||||
@@ -876,6 +905,48 @@ class Helper
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file is a video, so we can show a preview
|
||||
*
|
||||
* @param File $file
|
||||
* @return string | Boolean
|
||||
* @author [B. Wetherington] [<bwetherington@grokability.com>]
|
||||
* @since [v8.1.18]
|
||||
*/
|
||||
public static function checkUploadIsVideo($file)
|
||||
{
|
||||
$finfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
|
||||
$filetype = @finfo_file($finfo, $file);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (($filetype == 'video/mp4') || ($filetype == 'video/quicktime') || ($filetype == 'video/mpeg') || ($filetype == 'video/ogg') || ($filetype == 'video/webm') || ($filetype == 'video/x-msvide')) {
|
||||
return $filetype;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file is audio, so we can show a preview
|
||||
*
|
||||
* @param File $file
|
||||
* @return string | Boolean
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v3.0]
|
||||
*/
|
||||
public static function checkUploadIsAudio($file)
|
||||
{
|
||||
$finfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
|
||||
$filetype = @finfo_file($finfo, $file);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (($filetype == 'audio/mpeg') || ($filetype == 'audio/ogg')) {
|
||||
return $filetype;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks through the permissions in the permissions config file and determines if
|
||||
* permissions are granted based on a $selected_arr array.
|
||||
@@ -1154,22 +1225,42 @@ class Helper
|
||||
'webp' => 'far fa-image',
|
||||
'avif' => 'far fa-image',
|
||||
'svg' => 'fas fa-vector-square',
|
||||
|
||||
// word
|
||||
'doc' => 'far fa-file-word',
|
||||
'docx' => 'far fa-file-word',
|
||||
|
||||
// Excel
|
||||
'xls' => 'far fa-file-excel',
|
||||
'xlsx' => 'far fa-file-excel',
|
||||
'ods' => 'far fa-file-excel',
|
||||
|
||||
// Presentation
|
||||
'ppt' => 'far fa-file-powerpoint',
|
||||
'odp' => 'far fa-file-powerpoint',
|
||||
|
||||
// archive
|
||||
'zip' => 'fas fa-file-archive',
|
||||
'rar' => 'fas fa-file-archive',
|
||||
|
||||
//Text
|
||||
'odt' => 'far fa-file-alt',
|
||||
'txt' => 'far fa-file-alt',
|
||||
'rtf' => 'far fa-file-alt',
|
||||
'xml' => 'fas fa-code',
|
||||
|
||||
// Misc
|
||||
'pdf' => 'far fa-file-pdf',
|
||||
'lic' => 'far fa-save',
|
||||
|
||||
// video
|
||||
'mov' => 'fa-solid fa-video',
|
||||
'mp4' => 'fa-solid fa-video',
|
||||
|
||||
// audio
|
||||
'ogg' => 'fa-solid fa-file-audio',
|
||||
'mp3' => 'fa-solid fa-file-audio',
|
||||
'wav' => 'fa-solid fa-file-audio',
|
||||
];
|
||||
|
||||
if ($extension && array_key_exists($extension, $allowedExtensionMap)) {
|
||||
@@ -1480,68 +1571,59 @@ class Helper
|
||||
}
|
||||
|
||||
|
||||
static public function getRedirectOption($request, $id, $table, $item_id = null)
|
||||
static public function getRedirectOption($request, $id, $table, $item_id = null) : RedirectResponse
|
||||
{
|
||||
|
||||
$redirect_option = Session::get('redirect_option');
|
||||
$checkout_to_type = Session::get('checkout_to_type');
|
||||
$redirect_option = Session::get('redirect_option') ?? $request->redirect_option;
|
||||
$checkout_to_type = Session::get('checkout_to_type') ?? null;
|
||||
$checkedInFrom = Session::get('checkedInFrom');
|
||||
$other_redirect = Session::get('other_redirect');
|
||||
$backUrl = Session::pull('back_url', route('home'));
|
||||
|
||||
// return to previous page
|
||||
if ($redirect_option === 'back') {
|
||||
return redirect()->to($backUrl);
|
||||
}
|
||||
|
||||
// return to index
|
||||
if ($redirect_option == 'index') {
|
||||
switch ($table) {
|
||||
case "Assets":
|
||||
return route('hardware.index');
|
||||
case "Users":
|
||||
return route('users.index');
|
||||
case "Licenses":
|
||||
return route('licenses.index');
|
||||
case "Accessories":
|
||||
return route('accessories.index');
|
||||
case "Components":
|
||||
return route('components.index');
|
||||
case "Consumables":
|
||||
return route('consumables.index');
|
||||
}
|
||||
return match ($table) {
|
||||
'Assets' => redirect()->route('hardware.index'),
|
||||
'Users' => redirect()->route('users.index'),
|
||||
'Licenses' => redirect()->route('licenses.index'),
|
||||
'Accessories' => redirect()->route('accessories.index'),
|
||||
'Components' => redirect()->route('components.index'),
|
||||
'Consumables' => redirect()->route('consumables.index'),
|
||||
};
|
||||
}
|
||||
|
||||
// return to thing being assigned
|
||||
if ($redirect_option == 'item') {
|
||||
switch ($table) {
|
||||
case "Assets":
|
||||
return route('hardware.show', $id ?? $item_id);
|
||||
case "Users":
|
||||
return route('users.show', $id ?? $item_id);
|
||||
case "Licenses":
|
||||
return route('licenses.show', $id ?? $item_id);
|
||||
case "Accessories":
|
||||
return route('accessories.show', $id ?? $item_id);
|
||||
case "Components":
|
||||
return route('components.show', $id ?? $item_id);
|
||||
case "Consumables":
|
||||
return route('consumables.show', $id ?? $item_id);
|
||||
}
|
||||
return match ($table) {
|
||||
'Assets' => redirect()->route('hardware.show', $id ?? $item_id),
|
||||
'Users' => redirect()->route('users.show', $id ?? $item_id),
|
||||
'Licenses' => redirect()->route('licenses.show', $id ?? $item_id),
|
||||
'Accessories' => redirect()->route('accessories.show', $id ?? $item_id),
|
||||
'Components' => redirect()->route('components.show', $id ?? $item_id),
|
||||
'Consumables' => redirect()->route('consumables.show', $id ?? $item_id),
|
||||
};
|
||||
}
|
||||
|
||||
// return to assignment target
|
||||
if ($redirect_option == 'target') {
|
||||
switch ($checkout_to_type) {
|
||||
case 'user':
|
||||
return route('users.show', $request->assigned_user ?? $checkedInFrom);
|
||||
case 'location':
|
||||
return route('locations.show', $request->assigned_location ?? $checkedInFrom);
|
||||
case 'asset':
|
||||
return route('hardware.show', $request->assigned_asset ?? $checkedInFrom);
|
||||
}
|
||||
return match ($checkout_to_type) {
|
||||
'user' => redirect()->route('users.show', $request->assigned_user ?? $checkedInFrom),
|
||||
'location' => redirect()->route('locations.show', $request->assigned_location ?? $checkedInFrom),
|
||||
'asset' => redirect()->route('hardware.show', $request->assigned_asset ?? $checkedInFrom),
|
||||
};
|
||||
}
|
||||
|
||||
// return to somewhere else
|
||||
if ($redirect_option == 'other_redirect') {
|
||||
switch ($other_redirect) {
|
||||
case 'audit':
|
||||
return route('assets.audit.due');
|
||||
}
|
||||
return match ($other_redirect) {
|
||||
'audit' => redirect()->route('assets.audit.due'),
|
||||
'model' => redirect()->route('models.show', $request->model_id),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class IconHelper
|
||||
case 'clone':
|
||||
return 'far fa-clone';
|
||||
case 'delete':
|
||||
case 'upload deleted':
|
||||
return 'fas fa-trash';
|
||||
case 'create':
|
||||
return 'fa-solid fa-plus';
|
||||
@@ -39,10 +40,14 @@ class IconHelper
|
||||
return 'fa-solid fa-trash-arrow-up';
|
||||
case 'external-link':
|
||||
return 'fa fa-external-link';
|
||||
case 'link':
|
||||
return 'fa fa-link';
|
||||
case 'email':
|
||||
return 'fa-regular fa-envelope';
|
||||
case 'phone':
|
||||
return 'fa-solid fa-phone';
|
||||
case 'mobile':
|
||||
return 'fas fa-mobile-screen-button';
|
||||
case 'long-arrow-right':
|
||||
return 'fas fa-long-arrow-alt-right';
|
||||
case 'download':
|
||||
@@ -137,7 +142,7 @@ class IconHelper
|
||||
case 'more-files':
|
||||
return 'fa-solid fa-laptop-file';
|
||||
case 'maintenances':
|
||||
return 'fas fa-wrench';
|
||||
return 'fa-solid fa-screwdriver-wrench';
|
||||
case 'seats':
|
||||
return 'far fa-list-alt';
|
||||
case 'globe-us':
|
||||
@@ -151,6 +156,7 @@ class IconHelper
|
||||
case 'location':
|
||||
return 'fas fa-map-marker-alt';
|
||||
case 'superadmin':
|
||||
case 'admin':
|
||||
return 'fas fa-crown';
|
||||
case 'print':
|
||||
return 'fa-solid fa-print';
|
||||
@@ -191,6 +197,10 @@ class IconHelper
|
||||
case 'note':
|
||||
case 'notes':
|
||||
return 'fas fa-sticky-note';
|
||||
case 'tip':
|
||||
return 'fa-solid fa-lightbulb';
|
||||
case 'highlight':
|
||||
return 'fa-solid fa-highlighter';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,38 +16,84 @@ class StorageHelper
|
||||
$disk = config('filesystems.default');
|
||||
}
|
||||
switch (config("filesystems.disks.$disk.driver")) {
|
||||
case 'local':
|
||||
return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
|
||||
case 'local':
|
||||
return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
|
||||
|
||||
case 's3':
|
||||
return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
|
||||
case 's3':
|
||||
return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
|
||||
|
||||
default:
|
||||
return Storage::disk($disk)->download($filename);
|
||||
default:
|
||||
return Storage::disk($disk)->download($filename);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getMediaType($file_with_path) {
|
||||
|
||||
// Get the file extension and determine the media type
|
||||
if (Storage::exists($file_with_path)) {
|
||||
$fileinfo = pathinfo($file_with_path);
|
||||
$extension = strtolower($fileinfo['extension']);
|
||||
switch ($extension) {
|
||||
case 'avif':
|
||||
case 'jpg':
|
||||
case 'png':
|
||||
case 'gif':
|
||||
case 'svg':
|
||||
case 'webp':
|
||||
return 'image';
|
||||
case 'pdf':
|
||||
return 'pdf';
|
||||
case 'mp3':
|
||||
case 'wav':
|
||||
case 'ogg':
|
||||
return 'audio';
|
||||
case 'mp4':
|
||||
case 'webm':
|
||||
case 'mov':
|
||||
return 'video';
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return 'document';
|
||||
case 'txt':
|
||||
return 'text';
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
case 'ods':
|
||||
return 'spreadsheet';
|
||||
default:
|
||||
return $extension; // Default for unknown types
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This determines the file types that should be allowed inline and checks their fileinfo extension
|
||||
* to determine that they are safe to display inline.
|
||||
*
|
||||
* @author <A. Gianotto> [<snipe@snipe.net]>
|
||||
* @since v7.0.14
|
||||
* @param $file_with_path
|
||||
* @since v7.0.14
|
||||
* @param $file_with_path
|
||||
* @return bool
|
||||
*/
|
||||
public static function allowSafeInline($file_with_path) {
|
||||
public static function allowSafeInline($file_with_path)
|
||||
{
|
||||
|
||||
$allowed_inline = [
|
||||
'pdf',
|
||||
'svg',
|
||||
'jpg',
|
||||
'gif',
|
||||
'svg',
|
||||
'avif',
|
||||
'webp',
|
||||
'gif',
|
||||
'gif',
|
||||
'jpg',
|
||||
'mov',
|
||||
'mp3',
|
||||
'mp4',
|
||||
'ogg',
|
||||
'pdf',
|
||||
'png',
|
||||
'svg',
|
||||
'wav',
|
||||
'webm',
|
||||
'webp',
|
||||
];
|
||||
|
||||
|
||||
@@ -59,10 +105,24 @@ class StorageHelper
|
||||
|
||||
}
|
||||
|
||||
public static function getFiletype($file_with_path)
|
||||
{
|
||||
|
||||
// The file exists and is allowed to be displayed inline
|
||||
if (Storage::exists($file_with_path)) {
|
||||
return pathinfo($file_with_path, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decide whether to show the file inline or download it.
|
||||
*/
|
||||
public static function showOrDownloadFile($file, $filename) {
|
||||
public static function showOrDownloadFile($file, $filename)
|
||||
{
|
||||
|
||||
$headers = [];
|
||||
|
||||
|
||||
@@ -77,13 +77,30 @@ class AccessoriesController extends Controller
|
||||
$accessory->supplier_id = request('supplier_id');
|
||||
$accessory->notes = request('notes');
|
||||
|
||||
$accessory = $request->handleImages($accessory);
|
||||
if ($request->has('use_cloned_image')) {
|
||||
$cloned_model_img = Accessory::select('image')->find($request->input('clone_image_from_id'));
|
||||
if ($cloned_model_img) {
|
||||
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
|
||||
$new_image = 'accessories/'.$new_image_name;
|
||||
Storage::disk('public')->copy('accessories/'.$cloned_model_img->image, $new_image);
|
||||
$accessory->image = $new_image_name;
|
||||
}
|
||||
|
||||
} else {
|
||||
$accessory = $request->handleImages($accessory);
|
||||
}
|
||||
|
||||
if($request->get('redirect_option') === 'back'){
|
||||
session()->put(['redirect_option' => 'index']);
|
||||
} else {
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
}
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
// Was the accessory created?
|
||||
if ($accessory->save()) {
|
||||
// Redirect to the new accessory page
|
||||
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.create.success'));
|
||||
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
|
||||
->with('success', trans('admin/accessories/message.create.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->withErrors($accessory->getErrors());
|
||||
@@ -113,11 +130,12 @@ class AccessoriesController extends Controller
|
||||
|
||||
$this->authorize('create', Accessory::class);
|
||||
$cloned = clone $accessory;
|
||||
$accessory_to_clone = $accessory;
|
||||
$cloned->id = null;
|
||||
$cloned->deleted_at = '';
|
||||
$cloned->location_id = null;
|
||||
|
||||
return view('accessories/edit')
|
||||
->with('cloned_model', $accessory_to_clone)
|
||||
->with('item', $cloned);
|
||||
|
||||
}
|
||||
@@ -167,7 +185,8 @@ class AccessoriesController extends Controller
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
|
||||
if ($accessory->save()) {
|
||||
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.update.success'));
|
||||
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
|
||||
->with('success', trans('admin/accessories/message.update.success'));
|
||||
}
|
||||
} else {
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Accessories;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Accessory;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use \Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class AccessoriesFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Validates and stores files associated with a accessory.
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
* @param int $accessoryId
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @todo Switch to using the AssetFileRequest form request validator.
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $accessoryId = null) : RedirectResponse
|
||||
{
|
||||
|
||||
if (config('app.lock_passwords')) {
|
||||
return redirect()->route('accessories.show', ['accessory'=>$accessoryId])->with('error', trans('general.feature_disabled'));
|
||||
}
|
||||
|
||||
$accessory = Accessory::find($accessoryId);
|
||||
|
||||
if (isset($accessory->id)) {
|
||||
$this->authorize('accessories.files', $accessory);
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
if (! Storage::exists('private_uploads/accessories')) {
|
||||
Storage::makeDirectory('private_uploads/accessories', 775);
|
||||
}
|
||||
|
||||
foreach ($request->file('file') as $file) {
|
||||
|
||||
$file_name = $request->handleFile('private_uploads/accessories/', 'accessory-'.$accessory->id, $file);
|
||||
//Log the upload to the log
|
||||
$accessory->logUpload($file_name, e($request->input('notes')));
|
||||
}
|
||||
|
||||
|
||||
return redirect()->route('accessories.show', $accessory->id)->withFragment('files')->with('success', trans('general.file_upload_success'));
|
||||
|
||||
}
|
||||
|
||||
return redirect()->route('accessories.show', $accessory->id)->withFragment('files')->with('error', trans('general.no_files_uploaded'));
|
||||
}
|
||||
// Prepare the error message
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the selected accessory file.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @param int $accessoryId
|
||||
* @param int $fileId
|
||||
*/
|
||||
public function destroy($accessoryId = null, $fileId = null) : RedirectResponse
|
||||
{
|
||||
if ($accessory = Accessory::find($accessoryId)) {
|
||||
$this->authorize('update', $accessory);
|
||||
|
||||
if ($log = Actionlog::find($fileId)) {
|
||||
|
||||
if (Storage::exists('private_uploads/accessories/'.$log->filename)) {
|
||||
try {
|
||||
Storage::delete('private_uploads/accessories/' . $log->filename);
|
||||
$log->delete();
|
||||
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
|
||||
} catch (\Exception $e) {
|
||||
Log::debug($e);
|
||||
return redirect()->route('accessories.index')->with('error', trans('general.file_does_not_exist'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return redirect()->route('accessories.show', ['accessory' => $accessory])->withFragment('files')->with('error', trans('general.log_record_not_found'));
|
||||
}
|
||||
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the selected file to be viewed.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.4]
|
||||
* @param int $accessoryId
|
||||
* @param int $fileId
|
||||
*/
|
||||
public function show($accessoryId = null, $fileId = null) : View | RedirectResponse | Response | BinaryFileResponse | StreamedResponse
|
||||
{
|
||||
|
||||
|
||||
// the accessory is valid
|
||||
if ($accessory = Accessory::find($accessoryId)) {
|
||||
$this->authorize('view', $accessory);
|
||||
$this->authorize('accessories.files', $accessory);
|
||||
|
||||
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $accessory->id)->find($fileId)) {
|
||||
$file = 'private_uploads/accessories/'.$log->filename;
|
||||
|
||||
try {
|
||||
return StorageHelper::showOrDownloadFile($file, $log->filename);
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->route('accessories.show', ['accessory' => $accessory])->with('error', trans('general.file_not_found'));
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('accessories.show', ['accessory' => $accessory])->withFragment('files')->with('error', trans('general.log_record_not_found'));
|
||||
|
||||
}
|
||||
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,8 @@ class AccessoryCheckinController extends Controller
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
|
||||
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.checkin.success'));
|
||||
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
|
||||
->with('success', trans('admin/accessories/message.checkin.success'));
|
||||
}
|
||||
// Redirect to the accessory management page with error
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkin.error'));
|
||||
|
||||
@@ -71,6 +71,7 @@ class AccessoryCheckoutController extends Controller
|
||||
$this->authorize('checkout', $accessory);
|
||||
|
||||
$target = $this->determineCheckoutTarget();
|
||||
session()->put(['checkout_to_type' => $target]);
|
||||
|
||||
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
||||
|
||||
@@ -97,7 +98,7 @@ class AccessoryCheckoutController extends Controller
|
||||
|
||||
|
||||
// Redirect to the new accessory page
|
||||
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))
|
||||
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
|
||||
->with('success', trans('admin/accessories/message.checkout.success'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,30 +7,24 @@ use App\Events\CheckoutDeclined;
|
||||
use App\Events\ItemAccepted;
|
||||
use App\Events\ItemDeclined;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Mail\CheckoutAcceptanceResponseMail;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\Company;
|
||||
use App\Models\Contracts\Acceptable;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\License;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Notifications\AcceptanceAssetAcceptedNotification;
|
||||
use App\Notifications\AcceptanceAssetAcceptedToUserNotification;
|
||||
use App\Notifications\AcceptanceAssetDeclinedNotification;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Http\Controllers\SettingsController;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Carbon\Carbon;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use \Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Helpers\Helper;
|
||||
|
||||
class AcceptanceController extends Controller
|
||||
{
|
||||
@@ -81,6 +75,10 @@ class AcceptanceController extends Controller
|
||||
public function store(Request $request, $id) : RedirectResponse
|
||||
{
|
||||
$acceptance = CheckoutAcceptance::find($id);
|
||||
$assigned_user = User::find($acceptance->assigned_to_id);
|
||||
$settings = Setting::getSettings();
|
||||
$sig_filename='';
|
||||
|
||||
|
||||
if (is_null($acceptance)) {
|
||||
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||
@@ -103,145 +101,95 @@ class AcceptanceController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the signature and save it
|
||||
* Check for the signature directory
|
||||
*/
|
||||
if (! Storage::exists('private_uploads/signatures')) {
|
||||
Storage::makeDirectory('private_uploads/signatures', 775);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check for the eula-pdfs directory
|
||||
*/
|
||||
if (! Storage::exists('private_uploads/eula-pdfs')) {
|
||||
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
|
||||
}
|
||||
|
||||
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
|
||||
$display_model = '';
|
||||
$pdf_view_route = '';
|
||||
$pdf_filename = 'accepted-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
||||
$sig_filename='';
|
||||
|
||||
// If signatures are required, make sure we have one
|
||||
if (Setting::getSettings()->require_accept_signature == '1') {
|
||||
|
||||
// The item was accepted, check for a signature
|
||||
if ($request->filled('signature_output')) {
|
||||
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
|
||||
$data_uri = $request->input('signature_output');
|
||||
$encoded_image = explode(',', $data_uri);
|
||||
$decoded_image = base64_decode($encoded_image[1]);
|
||||
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
|
||||
|
||||
// No image data is present, kick them back.
|
||||
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
|
||||
} else {
|
||||
return redirect()->back()->with('error', trans('general.shitty_browser'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Convert PDF logo to base64 for TCPDF
|
||||
// This is needed for TCPDF to properly embed the image if it's a png and the cache isn't writable
|
||||
$encoded_logo = null;
|
||||
if ($settings->acceptance_pdf_logo) {
|
||||
$encoded_logo = base64_encode(file_get_contents(public_path() . '/uploads/' . $settings->acceptance_pdf_logo));
|
||||
}
|
||||
|
||||
// Get the data array ready for the notifications and PDF generation
|
||||
$data = [
|
||||
'item_tag' => $item->asset_tag,
|
||||
'item_name' => $item->name, // this handles licenses seats, which don't have a 'name' field
|
||||
'item_model' => $item->model?->name,
|
||||
'item_serial' => $item->serial,
|
||||
'item_status' => $item->assetstatus?->name,
|
||||
'eula' => $item->getEula(),
|
||||
'note' => $request->input('note'),
|
||||
'check_out_date' => Helper::getFormattedDateObject($acceptance->created_at, 'datetime', false),
|
||||
'accepted_date' => Helper::getFormattedDateObject(now()->format('Y-m-d H:i:s'), 'datetime', false),
|
||||
'declined_date' => Helper::getFormattedDateObject(now()->format('Y-m-d H:i:s'), 'datetime', false),
|
||||
'assigned_to' => $assigned_user->display_name,
|
||||
'email' => $assigned_user->email,
|
||||
'employee_num' => $assigned_user->employee_num,
|
||||
'site_name' => $settings->site_name,
|
||||
'company_name' => $item->company?->name?? $settings->site_name,
|
||||
'signature' => (($sig_filename && array_key_exists('1', $encoded_image))) ? $encoded_image[1] : null,
|
||||
'logo' => ($encoded_logo) ?? null,
|
||||
'date_settings' => $settings->date_display_format,
|
||||
'qty' => $acceptance->qty ?? 1,
|
||||
];
|
||||
|
||||
if ($request->input('asset_acceptance') == 'accepted') {
|
||||
|
||||
/**
|
||||
* Check for the eula-pdfs directory
|
||||
*/
|
||||
if (! Storage::exists('private_uploads/eula-pdfs')) {
|
||||
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
|
||||
}
|
||||
|
||||
if (Setting::getSettings()->require_accept_signature == '1') {
|
||||
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
||||
|
||||
// Check if the signature directory exists, if not create it
|
||||
if (!Storage::exists('private_uploads/signatures')) {
|
||||
Storage::makeDirectory('private_uploads/signatures', 775);
|
||||
}
|
||||
|
||||
// The item was accepted, check for a signature
|
||||
if ($request->filled('signature_output')) {
|
||||
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
|
||||
$data_uri = $request->input('signature_output');
|
||||
$encoded_image = explode(',', $data_uri);
|
||||
$decoded_image = base64_decode($encoded_image[1]);
|
||||
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
|
||||
|
||||
// No image data is present, kick them back.
|
||||
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
|
||||
} else {
|
||||
return redirect()->back()->with('error', trans('general.shitty_browser'));
|
||||
}
|
||||
}
|
||||
|
||||
// this is horrible
|
||||
switch($acceptance->checkoutable_type){
|
||||
case 'App\Models\Asset':
|
||||
$pdf_view_route ='account.accept.accept-asset-eula';
|
||||
$asset_model = AssetModel::find($item->model_id);
|
||||
if (!$asset_model) {
|
||||
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
|
||||
}
|
||||
$display_model = $asset_model->name;
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
break;
|
||||
|
||||
case 'App\Models\Accessory':
|
||||
$pdf_view_route ='account.accept.accept-accessory-eula';
|
||||
$accessory = Accessory::find($item->id);
|
||||
$display_model = $accessory->name;
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
break;
|
||||
|
||||
case 'App\Models\LicenseSeat':
|
||||
$pdf_view_route ='account.accept.accept-license-eula';
|
||||
$license = License::find($item->license_id);
|
||||
$display_model = $license->name;
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
break;
|
||||
|
||||
case 'App\Models\Component':
|
||||
$pdf_view_route ='account.accept.accept-component-eula';
|
||||
$component = Component::find($item->id);
|
||||
$display_model = $component->name;
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
break;
|
||||
|
||||
case 'App\Models\Consumable':
|
||||
$pdf_view_route ='account.accept.accept-consumable-eula';
|
||||
$consumable = Consumable::find($item->id);
|
||||
$display_model = $consumable->name;
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
break;
|
||||
}
|
||||
// if ($acceptance->checkoutable_type == 'App\Models\Asset') {
|
||||
// $pdf_view_route ='account.accept.accept-asset-eula';
|
||||
// $asset_model = AssetModel::find($item->model_id);
|
||||
// $display_model = $asset_model->name;
|
||||
// $assigned_to = User::find($item->assigned_to)->present()->fullName;
|
||||
//
|
||||
// } elseif ($acceptance->checkoutable_type== 'App\Models\Accessory') {
|
||||
// $pdf_view_route ='account.accept.accept-accessory-eula';
|
||||
// $accessory = Accessory::find($item->id);
|
||||
// $display_model = $accessory->name;
|
||||
// $assigned_to = User::find($item->assignedTo);
|
||||
//
|
||||
// }
|
||||
|
||||
/**
|
||||
* Gather the data for the PDF. We fire this whether there is a signature required or not,
|
||||
* since we want the moment-in-time proof of what the EULA was when they accepted it.
|
||||
*/
|
||||
$branding_settings = SettingsController::getPDFBranding();
|
||||
|
||||
$path_logo = "";
|
||||
|
||||
// Check for the PDF logo path and use that, otherwise use the regular logo path
|
||||
if (!is_null($branding_settings->acceptance_pdf_logo)) {
|
||||
$path_logo = public_path() . '/uploads/' . $branding_settings->acceptance_pdf_logo;
|
||||
} elseif (!is_null($branding_settings->logo)) {
|
||||
$path_logo = public_path() . '/uploads/' . $branding_settings->logo;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'item_tag' => $item->asset_tag,
|
||||
'item_model' => $display_model,
|
||||
'item_serial' => $item->serial,
|
||||
'item_status' => $item->assetstatus?->name,
|
||||
'eula' => $item->getEula(),
|
||||
'note' => $request->input('note'),
|
||||
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d'),
|
||||
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d'),
|
||||
'assigned_to' => $assigned_to,
|
||||
'company_name' => $branding_settings->site_name,
|
||||
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
|
||||
'logo' => $path_logo,
|
||||
'date_settings' => $branding_settings->date_display_format,
|
||||
];
|
||||
|
||||
if ($pdf_view_route!='') {
|
||||
Log::debug($pdf_filename.' is the filename, and the route was specified.');
|
||||
$pdf = Pdf::loadView($pdf_view_route, $data);
|
||||
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf->output());
|
||||
}
|
||||
// Generate the PDF content
|
||||
$pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
|
||||
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf_content);
|
||||
|
||||
// Log the acceptance
|
||||
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
|
||||
|
||||
// Send the PDF to the signing user
|
||||
if (($request->input('send_copy') == '1') && ($assigned_user->email !='')) {
|
||||
|
||||
// Add the attachment for the signing user into the $data array
|
||||
$data['file'] = $pdf_filename;
|
||||
try {
|
||||
$assigned_user->notify((new AcceptanceAssetAcceptedToUserNotification($data))->locale($assigned_user->locale));
|
||||
} catch (\Exception $e) {
|
||||
Log::warning($e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
$acceptance->notify(new AcceptanceAssetAcceptedNotification($data));
|
||||
$acceptance->notify((new AcceptanceAssetAcceptedNotification($data))->locale(Setting::getSettings()->locale));
|
||||
} catch (\Exception $e) {
|
||||
Log::warning($e);
|
||||
}
|
||||
@@ -249,97 +197,43 @@ class AcceptanceController extends Controller
|
||||
|
||||
$return_msg = trans('admin/users/message.accepted');
|
||||
|
||||
// Item was declined
|
||||
} else {
|
||||
|
||||
/**
|
||||
* Check for the eula-pdfs directory
|
||||
*/
|
||||
if (! Storage::exists('private_uploads/eula-pdfs')) {
|
||||
Storage::makeDirectory('private_uploads/eula-pdfs', 775);
|
||||
for ($i = 0; $i < ($acceptance->qty ?? 1); $i++) {
|
||||
$acceptance->decline($sig_filename, $request->input('note'));
|
||||
}
|
||||
|
||||
if (Setting::getSettings()->require_accept_signature == '1') {
|
||||
|
||||
// Check if the signature directory exists, if not create it
|
||||
if (!Storage::exists('private_uploads/signatures')) {
|
||||
Storage::makeDirectory('private_uploads/signatures', 775);
|
||||
}
|
||||
|
||||
// The item was accepted, check for a signature
|
||||
if ($request->filled('signature_output')) {
|
||||
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
|
||||
$data_uri = $request->input('signature_output');
|
||||
$encoded_image = explode(',', $data_uri);
|
||||
$decoded_image = base64_decode($encoded_image[1]);
|
||||
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
|
||||
|
||||
// No image data is present, kick them back.
|
||||
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
|
||||
} else {
|
||||
return redirect()->back()->with('error', trans('general.shitty_browser'));
|
||||
}
|
||||
}
|
||||
|
||||
// Format the data to send the declined notification
|
||||
$branding_settings = SettingsController::getPDFBranding();
|
||||
|
||||
// This is the most horriblest
|
||||
switch($acceptance->checkoutable_type){
|
||||
case 'App\Models\Asset':
|
||||
$asset_model = AssetModel::find($item->model_id);
|
||||
$display_model = $asset_model->name;
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
break;
|
||||
|
||||
case 'App\Models\Accessory':
|
||||
$accessory = Accessory::find($item->id);
|
||||
$display_model = $accessory->name;
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
break;
|
||||
|
||||
case 'App\Models\LicenseSeat':
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
break;
|
||||
|
||||
case 'App\Models\Component':
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
break;
|
||||
|
||||
case 'App\Models\Consumable':
|
||||
$consumable = Consumable::find($item->id);
|
||||
$display_model = $consumable->name;
|
||||
$assigned_to = User::find($acceptance->assigned_to_id)->present()->fullName;
|
||||
break;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'item_tag' => $item->asset_tag,
|
||||
'item_model' => $display_model,
|
||||
'item_serial' => $item->serial,
|
||||
'item_status' => $item->assetstatus?->name,
|
||||
'note' => $request->input('note'),
|
||||
'declined_date' => Carbon::parse($acceptance->declined_at)->format('Y-m-d'),
|
||||
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
|
||||
'assigned_to' => $assigned_to,
|
||||
'company_name' => $branding_settings->site_name,
|
||||
'date_settings' => $branding_settings->date_display_format,
|
||||
];
|
||||
|
||||
if ($pdf_view_route!='') {
|
||||
Log::debug($pdf_filename.' is the filename, and the route was specified.');
|
||||
$pdf = Pdf::loadView($pdf_view_route, $data);
|
||||
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf->output());
|
||||
}
|
||||
|
||||
$acceptance->decline($sig_filename, $request->input('note'));
|
||||
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
|
||||
Log::debug('New event acceptance.');
|
||||
event(new CheckoutDeclined($acceptance));
|
||||
$return_msg = trans('admin/users/message.declined');
|
||||
}
|
||||
|
||||
|
||||
// Send an email notification if one is requested
|
||||
if ($acceptance->alert_on_response_id) {
|
||||
try {
|
||||
$recipient = User::find($acceptance->alert_on_response_id);
|
||||
|
||||
if ($recipient) {
|
||||
Log::debug('Attempting to send email acceptance.');
|
||||
Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail(
|
||||
$acceptance,
|
||||
$recipient,
|
||||
$request->input('asset_acceptance') === 'accepted',
|
||||
));
|
||||
Log::debug('Send email notification sucess on checkout acceptance response.');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
Log::warning($e);
|
||||
}
|
||||
}
|
||||
return redirect()->to('account/accept')->with('success', $return_msg);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Models\Actionlog;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use \Illuminate\Http\Response;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use \Illuminate\Http\Response;
|
||||
|
||||
class ActionlogController extends Controller
|
||||
{
|
||||
public function displaySig($filename) : RedirectResponse | Response | bool
|
||||
@@ -39,17 +41,29 @@ class ActionlogController extends Controller
|
||||
|
||||
public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
|
||||
{
|
||||
$this->authorize('view', \App\Models\Asset::class);
|
||||
|
||||
if (config('filesystems.default') == 's3_private') {
|
||||
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/'.$filename, now()->addMinutes(5)));
|
||||
if ($actionlog = Actionlog::where('filename', $filename)->with('user')->with('target')->firstOrFail()) {
|
||||
|
||||
$this->authorize('view', $actionlog->target);
|
||||
$this->authorize('view', $actionlog->user);
|
||||
|
||||
|
||||
if (config('filesystems.default') == 's3_private') {
|
||||
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/' . $filename, now()->addMinutes(5)));
|
||||
}
|
||||
|
||||
if (Storage::exists('private_uploads/eula-pdfs/' . $filename)) {
|
||||
|
||||
if (request()->input('inline') == 'true') {
|
||||
return response()->file(config('app.private_uploads') . '/eula-pdfs/' . $filename);
|
||||
}
|
||||
|
||||
return response()->download(config('app.private_uploads') . '/eula-pdfs/' . $filename);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', trans('general.file_does_not_exist'));
|
||||
}
|
||||
|
||||
if (Storage::exists('private_uploads/eula-pdfs/'.$filename)) {
|
||||
return response()->download(config('app.private_uploads').'/eula-pdfs/'.$filename);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', trans('general.file_does_not_exist'));
|
||||
|
||||
return redirect()->back()->with('error', trans('general.record_not_found'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,15 @@ class AccessoriesController extends Controller
|
||||
'notes',
|
||||
'checkouts_count',
|
||||
'qty',
|
||||
// These are *relationships* so we wouldn't normally include them in this array,
|
||||
// since they would normally create a `column not found` error,
|
||||
// BUT we account for them in the ordering switch down at the end of this method
|
||||
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
|
||||
'company',
|
||||
'location',
|
||||
'category',
|
||||
'supplier',
|
||||
'manufacturer',
|
||||
];
|
||||
|
||||
|
||||
@@ -61,10 +70,23 @@ class AccessoriesController extends Controller
|
||||
->with('category', 'company', 'manufacturer', 'checkouts', 'location', 'supplier', 'adminuser')
|
||||
->withCount('checkouts as checkouts_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$accessories = $accessories->TextSearch($request->input('search'));
|
||||
$filter = [];
|
||||
|
||||
if ($request->filled('filter')) {
|
||||
$filter = json_decode($request->input('filter'), true);
|
||||
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
|
||||
return in_array($key, $allowed_columns);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
}
|
||||
|
||||
if ((! is_null($filter)) && (count($filter)) > 0) {
|
||||
$accessories->ByFilter($filter);
|
||||
} elseif ($request->filled('search')) {
|
||||
$accessories->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$accessories->where('accessories.company_id', '=', $request->input('company_id'));
|
||||
}
|
||||
@@ -288,32 +310,42 @@ class AccessoriesController extends Controller
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
|
||||
|
||||
$accessory_checkout->created_by = auth()->id();
|
||||
$accessory_checkout->save();
|
||||
|
||||
$payload = [
|
||||
'accessory_id' => $accessory->id,
|
||||
'assigned_to' => $target->id,
|
||||
'assigned_type' => $target::class,
|
||||
'note' => $request->input('note'),
|
||||
'created_by' => auth()->id(),
|
||||
'pivot' => $accessory_checkout->id,
|
||||
];
|
||||
}
|
||||
|
||||
// Set this value to be able to pass the qty through to the event
|
||||
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check in the item so that it can be checked out again to someone else
|
||||
*
|
||||
* @uses Accessory::checkin_email() to determine if an email can and should be sent
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param Request $request
|
||||
* @param int $accessoryUserId
|
||||
* @param string $backto
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @return JsonResponse
|
||||
* @uses Accessory::checkin_email() to determine if an email can and should be sent
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @internal param int $accessoryId
|
||||
*/
|
||||
public function checkin(Request $request, $accessoryUserId = null)
|
||||
{
|
||||
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist', ['id' => $accessoryUserId])));
|
||||
}
|
||||
|
||||
$accessory = Accessory::find($accessory_checkout->accessory_id);
|
||||
@@ -327,7 +359,14 @@ class AccessoriesController extends Controller
|
||||
$user = User::find($accessory_checkout->assigned_to);
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkin.success')));
|
||||
$payload = [
|
||||
'accessory_id' => $accessory->id,
|
||||
'note' => $request->input('note'),
|
||||
'created_by' => auth()->id(),
|
||||
'pivot' => $accessory_checkout->id,
|
||||
];
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkin.success')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.checkin.error')));
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Transformers\UploadedFilesTransformer;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Actionlog;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
/**
|
||||
* This class controls file related actions related
|
||||
* to assets for the Snipe-IT Asset Management application.
|
||||
*
|
||||
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
|
||||
*
|
||||
* @version v1.0
|
||||
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
|
||||
*/
|
||||
class AssetFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Accepts a POST to upload a file to the server.
|
||||
*
|
||||
* @param \App\Http\Requests\UploadFileRequest $request
|
||||
* @param int $assetId
|
||||
* @since [v6.0]
|
||||
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $assetId = null) : JsonResponse
|
||||
{
|
||||
// Start by checking if the asset being acted upon exists
|
||||
if (! $asset = Asset::find($assetId)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
|
||||
}
|
||||
|
||||
// Make sure we are allowed to update this asset
|
||||
$this->authorize('update', $asset);
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
// If the file storage directory doesn't exist; create it
|
||||
if (! Storage::exists('private_uploads/assets')) {
|
||||
Storage::makeDirectory('private_uploads/assets', 775);
|
||||
}
|
||||
|
||||
// Loop over the attached files and add them to the asset
|
||||
foreach ($request->file('file') as $file) {
|
||||
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
|
||||
|
||||
$asset->logUpload($file_name, e($request->get('notes')));
|
||||
}
|
||||
|
||||
// All done - report success
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.upload.success')));
|
||||
}
|
||||
|
||||
// We only reach here if no files were included in the POST, so tell the user this
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.upload.nofiles')), 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* List the files for an asset.
|
||||
*
|
||||
* @param int $assetId
|
||||
* @since [v6.0]
|
||||
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
|
||||
*/
|
||||
public function list(Asset $asset, Request $request) : JsonResponse | array
|
||||
{
|
||||
|
||||
$this->authorize('view', $asset);
|
||||
|
||||
$allowed_columns =
|
||||
[
|
||||
'id',
|
||||
'filename',
|
||||
'eol',
|
||||
'notes',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')->where('item_type', '=', Asset::class)->where('item_id', '=', $asset->id);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$files = $files->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $files->count()) ? $files->count() : abs($request->input('offset'));
|
||||
$limit = app('api_limit_value');
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
$files = $files->orderBy($sort, $order);
|
||||
|
||||
$files = $files->skip($offset)->take($limit)->get();
|
||||
return (new UploadedFilesTransformer())->transformFiles($files, $files->count());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for permissions and display the file.
|
||||
*
|
||||
* @param int $assetId
|
||||
* @param int $fileId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
* @since [v6.0]
|
||||
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
|
||||
*/
|
||||
public function show(Asset $asset, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
|
||||
{
|
||||
|
||||
// the asset is valid
|
||||
if (isset($asset->id)) {
|
||||
$this->authorize('view', $asset);
|
||||
|
||||
// Check that the file being requested exists for the asset
|
||||
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.no_match', ['id' => $fileId])), 404);
|
||||
}
|
||||
|
||||
// Form the full filename with path
|
||||
$file = 'private_uploads/assets/'.$log->filename;
|
||||
Log::debug('Checking for '.$file);
|
||||
|
||||
if ($log->action_type == 'audit') {
|
||||
$file = 'private_uploads/audits/'.$log->filename;
|
||||
}
|
||||
|
||||
// Check the file actually exists on the filesystem
|
||||
if (! Storage::exists($file)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.does_not_exist', ['id' => $fileId])), 404);
|
||||
}
|
||||
|
||||
if (request('inline') == 'true') {
|
||||
|
||||
$headers = [
|
||||
'Content-Disposition' => 'inline',
|
||||
];
|
||||
|
||||
return Storage::download($file, $log->filename, $headers);
|
||||
}
|
||||
|
||||
return StorageHelper::downloader($file);
|
||||
}
|
||||
|
||||
// Send back an error message
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error', ['id' => $fileId])), 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the associated file
|
||||
*
|
||||
* @param int $assetId
|
||||
* @param int $fileId
|
||||
* @since [v6.0]
|
||||
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
|
||||
*/
|
||||
public function destroy(Asset $asset, $fileId = null) : JsonResponse
|
||||
{
|
||||
|
||||
$rel_path = 'private_uploads/assets';
|
||||
|
||||
// the asset is valid
|
||||
if (isset($asset->id)) {
|
||||
$this->authorize('update', $asset);
|
||||
|
||||
// Check for the file
|
||||
$log = Actionlog::find($fileId);
|
||||
|
||||
if ($log) {
|
||||
// Check the file actually exists, and delete it
|
||||
if (Storage::exists($rel_path.'/'.$log->filename)) {
|
||||
Storage::delete($rel_path.'/'.$log->filename);
|
||||
}
|
||||
|
||||
// Delete the record of the file
|
||||
$log->delete();
|
||||
|
||||
// All deleting done - notify the user of success
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.deletefile.success')), 200);
|
||||
}
|
||||
|
||||
// The file doesn't seem to really exist, so report an error
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Actionlog;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Http\Transformers\AssetModelsTransformer;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
|
||||
/**
|
||||
* This class controls file related actions related
|
||||
* to assets for the Snipe-IT Asset Management application.
|
||||
*
|
||||
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
|
||||
*
|
||||
* @version v1.0
|
||||
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
|
||||
*/
|
||||
class AssetModelFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Accepts a POST to upload a file to the server.
|
||||
*
|
||||
* @param \App\Http\Requests\UploadFileRequest $request
|
||||
* @param int $assetModelId
|
||||
* @since [v7.0.12]
|
||||
* @author [r-xyz]
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $assetModelId = null) : JsonResponse
|
||||
{
|
||||
// Start by checking if the asset being acted upon exists
|
||||
if (! $assetModel = AssetModel::find($assetModelId)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
|
||||
}
|
||||
|
||||
// Make sure we are allowed to update this asset
|
||||
$this->authorize('update', $assetModel);
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
// If the file storage directory doesn't exist; create it
|
||||
if (! Storage::exists('private_uploads/assetmodels')) {
|
||||
Storage::makeDirectory('private_uploads/assetmodels', 775);
|
||||
}
|
||||
|
||||
// Loop over the attached files and add them to the asset
|
||||
foreach ($request->file('file') as $file) {
|
||||
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$assetModel->id, $file);
|
||||
|
||||
$assetModel->logUpload($file_name, e($request->get('notes')));
|
||||
}
|
||||
|
||||
// All done - report success
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $assetModel, trans('admin/models/message.upload.success')));
|
||||
}
|
||||
|
||||
// We only reach here if no files were included in the POST, so tell the user this
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.upload.nofiles')), 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* List the files for an asset.
|
||||
*
|
||||
* @param int $assetmodel
|
||||
* @since [v7.0.12]
|
||||
* @author [r-xyz]
|
||||
*/
|
||||
public function list($assetmodel_id) : JsonResponse | array
|
||||
{
|
||||
// Start by checking if the asset being acted upon exists
|
||||
if (! $assetModel = AssetModel::find($assetmodel_id)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
|
||||
}
|
||||
|
||||
$assetmodel = AssetModel::with('uploads')->find($assetmodel_id);
|
||||
$this->authorize('view', $assetmodel);
|
||||
return (new AssetModelsTransformer)->transformAssetModelFiles($assetmodel, $assetmodel->uploads()->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for permissions and display the file.
|
||||
*
|
||||
* @param int $assetModelId
|
||||
* @param int $fileId
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
* @since [v7.0.12]
|
||||
* @author [r-xyz]
|
||||
*/
|
||||
public function show($assetModelId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
|
||||
{
|
||||
// Start by checking if the asset being acted upon exists
|
||||
if (! $assetModel = AssetModel::find($assetModelId)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
|
||||
}
|
||||
|
||||
// the asset is valid
|
||||
if (isset($assetModel->id)) {
|
||||
$this->authorize('view', $assetModel);
|
||||
|
||||
// Check that the file being requested exists for the asset
|
||||
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $assetModel->id)->find($fileId)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.no_match', ['id' => $fileId])), 404);
|
||||
}
|
||||
|
||||
// Form the full filename with path
|
||||
$file = 'private_uploads/assetmodels/'.$log->filename;
|
||||
Log::debug('Checking for '.$file);
|
||||
|
||||
if ($log->action_type == 'audit') {
|
||||
$file = 'private_uploads/audits/'.$log->filename;
|
||||
}
|
||||
|
||||
// Check the file actually exists on the filesystem
|
||||
if (! Storage::exists($file)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.does_not_exist', ['id' => $fileId])), 404);
|
||||
}
|
||||
|
||||
if (request('inline') == 'true') {
|
||||
|
||||
$headers = [
|
||||
'Content-Disposition' => 'inline',
|
||||
];
|
||||
|
||||
return Storage::download($file, $log->filename, $headers);
|
||||
}
|
||||
|
||||
return StorageHelper::downloader($file);
|
||||
}
|
||||
|
||||
// Send back an error message
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error', ['id' => $fileId])), 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the associated file
|
||||
*
|
||||
* @param int $assetModelId
|
||||
* @param int $fileId
|
||||
* @since [v7.0.12]
|
||||
* @author [r-xyz]
|
||||
*/
|
||||
public function destroy($assetModelId = null, $fileId = null) : JsonResponse
|
||||
{
|
||||
// Start by checking if the asset being acted upon exists
|
||||
if (! $assetModel = AssetModel::find($assetModelId)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
|
||||
}
|
||||
|
||||
$rel_path = 'private_uploads/assetmodels';
|
||||
|
||||
// the asset is valid
|
||||
if (isset($assetModel->id)) {
|
||||
$this->authorize('update', $assetModel);
|
||||
|
||||
// Check for the file
|
||||
$log = Actionlog::find($fileId);
|
||||
if ($log) {
|
||||
// Check the file actually exists, and delete it
|
||||
if (Storage::exists($rel_path.'/'.$log->filename)) {
|
||||
Storage::delete($rel_path.'/'.$log->filename);
|
||||
}
|
||||
// Delete the record of the file
|
||||
$log->delete();
|
||||
|
||||
// All deleting done - notify the user of success
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/models/message.deletefile.success')), 200);
|
||||
}
|
||||
|
||||
// The file doesn't seem to really exist, so report an error
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
|
||||
}
|
||||
}
|
||||
@@ -46,10 +46,20 @@ class AssetModelsController extends Controller
|
||||
'manufacturer',
|
||||
'requestable',
|
||||
'assets_count',
|
||||
'assets_assigned_count',
|
||||
'assets_archived_count',
|
||||
'remaining',
|
||||
'category',
|
||||
'fieldset',
|
||||
'deleted_at',
|
||||
'updated_at',
|
||||
'require_serial',
|
||||
// These are *relationships* so we wouldn't normally include them in this array,
|
||||
// since they would normally create a `column not found` error,
|
||||
// BUT we account for them in the ordering switch down at the end of this method
|
||||
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
|
||||
'manufacturer',
|
||||
'category',
|
||||
];
|
||||
|
||||
$assetmodels = AssetModel::select([
|
||||
@@ -69,9 +79,31 @@ class AssetModelsController extends Controller
|
||||
'models.fieldset_id',
|
||||
'models.deleted_at',
|
||||
'models.updated_at',
|
||||
'models.require_serial'
|
||||
])
|
||||
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
|
||||
->withCount('assets as assets_count');
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('availableAssets as remaining')
|
||||
->withCount('assignedAssets as assets_assigned_count')
|
||||
->withCount('archivedAssets as assets_archived_count');
|
||||
|
||||
$filter = [];
|
||||
|
||||
if ($request->filled('filter')) {
|
||||
$filter = json_decode($request->input('filter'), true);
|
||||
|
||||
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
|
||||
return in_array($key, $allowed_columns);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
}
|
||||
|
||||
if ((! is_null($filter)) && (count($filter)) > 0) {
|
||||
$assetmodels->ByFilter($filter);
|
||||
} elseif ($request->filled('search')) {
|
||||
$assetmodels->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
|
||||
if ($request->input('status')=='deleted') {
|
||||
$assetmodels->onlyTrashed();
|
||||
@@ -154,7 +186,7 @@ class AssetModelsController extends Controller
|
||||
$assetmodel = $request->handleImages($assetmodel);
|
||||
|
||||
if ($assetmodel->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.create.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.create.success')));
|
||||
}
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
|
||||
|
||||
@@ -222,7 +254,7 @@ class AssetModelsController extends Controller
|
||||
|
||||
|
||||
if ($assetmodel->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.update.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.update.success')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Events\CheckoutableCheckedIn;
|
||||
use App\Http\Requests\StoreAssetRequest;
|
||||
use App\Http\Requests\UpdateAssetRequest;
|
||||
use App\Http\Traits\MigratesLegacyAssetLocations;
|
||||
use App\Http\Transformers\ComponentsTransformer;
|
||||
use App\Models\AccessoryCheckout;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\LicenseSeat;
|
||||
@@ -114,17 +115,40 @@ class AssetsController extends Controller
|
||||
'byod',
|
||||
'asset_eol_date',
|
||||
'requestable',
|
||||
'jobtitle',
|
||||
// These are *relationships* so we wouldn't normally include them in this array,
|
||||
// since they would normally create a `column not found` error,
|
||||
// BUT we account for them in the ordering switch down at the end of this method
|
||||
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
|
||||
'company',
|
||||
'model',
|
||||
'location',
|
||||
'rtd_location',
|
||||
'category',
|
||||
'status_label',
|
||||
'manufacturer',
|
||||
'supplier',
|
||||
'jobtitle',
|
||||
'assigned_to',
|
||||
'created_by',
|
||||
|
||||
];
|
||||
|
||||
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
|
||||
|
||||
foreach ($all_custom_fields as $field) {
|
||||
$allowed_columns[] = $field->db_column_name();
|
||||
}
|
||||
|
||||
$filter = [];
|
||||
|
||||
if ($request->filled('filter')) {
|
||||
$filter = json_decode($request->input('filter'), true);
|
||||
}
|
||||
|
||||
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
|
||||
foreach ($all_custom_fields as $field) {
|
||||
$allowed_columns[] = $field->db_column_name();
|
||||
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
|
||||
return in_array($key, $allowed_columns);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
}
|
||||
|
||||
$assets = Asset::select('assets.*')
|
||||
@@ -140,6 +164,7 @@ class AssetsController extends Controller
|
||||
'model.category',
|
||||
'model.manufacturer',
|
||||
'model.fieldset',
|
||||
'model.depreciation',
|
||||
'supplier'
|
||||
); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
|
||||
|
||||
@@ -158,7 +183,7 @@ class AssetsController extends Controller
|
||||
// Search custom fields by column name
|
||||
foreach ($all_custom_fields as $field) {
|
||||
if ($request->filled($field->db_column_name()) && $field->db_column_name()) {
|
||||
$assets->where($field->db_column_name(), '=', $request->input($field->db_column_name()));
|
||||
$assets->where('assets.'.$field->db_column_name(), '=', $request->input($field->db_column_name()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,6 +420,9 @@ class AssetsController extends Controller
|
||||
case 'assigned_to':
|
||||
$assets->OrderAssigned($order);
|
||||
break;
|
||||
case 'jobtitle':
|
||||
$assets->OrderByJobTitle($order);
|
||||
break;
|
||||
case 'created_by':
|
||||
$assets->OrderByCreatedByName($order);
|
||||
break;
|
||||
@@ -600,7 +628,7 @@ class AssetsController extends Controller
|
||||
$asset->use_text = $asset->present()->fullName;
|
||||
|
||||
if (($asset->checkedOutToUser()) && ($asset->assigned)) {
|
||||
$asset->use_text .= ' → ' . $asset->assigned->getFullNameAttribute();
|
||||
$asset->use_text .= ' → ' . $asset->assigned->display_name;
|
||||
}
|
||||
|
||||
|
||||
@@ -1280,9 +1308,19 @@ class AssetsController extends Controller
|
||||
|
||||
public function assignedAssets(Request $request, Asset $asset) : JsonResponse | array
|
||||
{
|
||||
$this->authorize('view', Asset::class);
|
||||
$this->authorize('view', $asset);
|
||||
|
||||
return [];
|
||||
// to do
|
||||
$query = Asset::where([
|
||||
'assigned_to' => $asset->id,
|
||||
'assigned_type' => Asset::class,
|
||||
]);
|
||||
|
||||
$total = $query->count();
|
||||
|
||||
$assets = $query->applyOffsetAndLimit($total)->get();
|
||||
|
||||
return (new AssetsTransformer)->transformAssets($assets, $total);
|
||||
}
|
||||
|
||||
public function assignedAccessories(Request $request, Asset $asset) : JsonResponse | array
|
||||
@@ -1302,6 +1340,18 @@ class AssetsController extends Controller
|
||||
return (new AssetsTransformer)->transformCheckedoutAccessories($accessory_checkouts, $total);
|
||||
}
|
||||
|
||||
public function assignedComponents(Request $request, Asset $asset): JsonResponse|array
|
||||
{
|
||||
$this->authorize('view', Asset::class);
|
||||
$this->authorize('view', $asset);
|
||||
|
||||
$asset->loadCount('components');
|
||||
$total = $asset->components_count;
|
||||
|
||||
$components = $asset->load(['components' => fn($query) => $query->applyOffsetAndLimit($total)])->components;
|
||||
|
||||
return (new ComponentsTransformer)->transformComponents($components, $total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate asset labels by tag
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Categories\DestroyCategoryAction;
|
||||
use App\Exceptions\ItemStillHasChildren;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\CategoriesTransformer;
|
||||
@@ -38,6 +40,8 @@ class CategoriesController extends Controller
|
||||
'consumables_count',
|
||||
'components_count',
|
||||
'licenses_count',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'image',
|
||||
'notes',
|
||||
];
|
||||
@@ -59,6 +63,23 @@ class CategoriesController extends Controller
|
||||
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count');
|
||||
|
||||
|
||||
$filter = [];
|
||||
|
||||
if ($request->filled('filter')) {
|
||||
$filter = json_decode($request->input('filter'), true);
|
||||
|
||||
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
|
||||
return in_array($key, $allowed_columns);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
}
|
||||
|
||||
if ((! is_null($filter)) && (count($filter)) > 0) {
|
||||
$categories->ByFilter($filter);
|
||||
} elseif ($request->filled('search')) {
|
||||
$categories->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
/*
|
||||
* This checks to see if we should override the Admin Setting to show archived assets in list.
|
||||
* We don't currently use it within the Snipe-IT GUI, but will be useful for API integrations where they
|
||||
@@ -72,10 +93,6 @@ class CategoriesController extends Controller
|
||||
$categories = $categories->withCount('showableAssets as assets_count');
|
||||
}
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$categories = $categories->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$categories->where('name', '=', $request->input('name'));
|
||||
}
|
||||
@@ -209,17 +226,21 @@ class CategoriesController extends Controller
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function destroy($id) : JsonResponse
|
||||
public function destroy(Category $category): JsonResponse
|
||||
{
|
||||
$this->authorize('delete', Category::class);
|
||||
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($id);
|
||||
|
||||
if (! $category->isDeletable()) {
|
||||
try {
|
||||
DestroyCategoryAction::run(category: $category);
|
||||
} catch (ItemStillHasChildren $e) {
|
||||
return response()->json(
|
||||
Helper::formatStandardApiResponse('error', null, trans('admin/categories/message.assoc_items', ['asset_type'=>$category->category_type]))
|
||||
Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.general_assoc_warning', ['asset_type' => $category->category_type]))
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
return response()->json(
|
||||
Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong'))
|
||||
);
|
||||
}
|
||||
$category->delete();
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/categories/message.delete.success')));
|
||||
}
|
||||
|
||||
@@ -43,7 +43,10 @@ class CompaniesController extends Controller
|
||||
|
||||
$companies = Company::withCount(['assets as assets_count' => function ($query) {
|
||||
$query->AssetsForShow();
|
||||
}])->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
|
||||
}])
|
||||
->with('adminuser')
|
||||
->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
|
||||
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$companies->TextSearch($request->input('search'));
|
||||
@@ -119,6 +122,7 @@ class CompaniesController extends Controller
|
||||
{
|
||||
$this->authorize('view', Company::class);
|
||||
$company = Company::findOrFail($id);
|
||||
$this->authorize('view', $company);
|
||||
return (new CompaniesTransformer)->transformCompany($company);
|
||||
|
||||
}
|
||||
@@ -136,6 +140,7 @@ class CompaniesController extends Controller
|
||||
{
|
||||
$this->authorize('update', Company::class);
|
||||
$company = Company::findOrFail($id);
|
||||
$this->authorize('update', $company);
|
||||
$company->fill($request->all());
|
||||
$company = $request->handleImages($company);
|
||||
|
||||
@@ -188,6 +193,7 @@ class CompaniesController extends Controller
|
||||
'companies.image',
|
||||
]);
|
||||
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->get('search').'%');
|
||||
}
|
||||
|
||||
@@ -45,16 +45,40 @@ class ComponentsController extends Controller
|
||||
'qty',
|
||||
'image',
|
||||
'notes',
|
||||
// These are *relationships* so we wouldn't normally include them in this array,
|
||||
// since they would normally create a `column not found` error,
|
||||
// BUT we account for them in the ordering switch down at the end of this method
|
||||
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
|
||||
'company',
|
||||
'location',
|
||||
'category',
|
||||
'manufacturer',
|
||||
'supplier',
|
||||
|
||||
];
|
||||
|
||||
$components = Component::select('components.*')
|
||||
->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer', 'uncontrainedAssets')
|
||||
->withSum('uncontrainedAssets', 'components_assets.assigned_qty');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$components = $components->TextSearch($request->input('search'));
|
||||
$filter = [];
|
||||
|
||||
if ($request->filled('filter')) {
|
||||
$filter = json_decode($request->input('filter'), true);
|
||||
|
||||
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
|
||||
return in_array($key, $allowed_columns);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
}
|
||||
|
||||
if ((! is_null($filter)) && (count($filter)) > 0) {
|
||||
$components->ByFilter($filter);
|
||||
} elseif ($request->filled('search')) {
|
||||
$components->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$components->where('name', '=', $request->input('name'));
|
||||
}
|
||||
|
||||
@@ -31,10 +31,53 @@ class ConsumablesController extends Controller
|
||||
$consumables = Consumable::with('company', 'location', 'category', 'supplier', 'manufacturer')
|
||||
->withCount('users as consumables_users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$consumables = $consumables->TextSearch(e($request->input('search')));
|
||||
// This array is what determines which fields should be allowed to be sorted on ON the table itself.
|
||||
// These must match a column on the consumables table directly.
|
||||
$allowed_columns = [
|
||||
'id',
|
||||
'name',
|
||||
'order_number',
|
||||
'min_amt',
|
||||
'purchase_date',
|
||||
'purchase_cost',
|
||||
'company',
|
||||
'category',
|
||||
'model_number',
|
||||
'item_no',
|
||||
'manufacturer',
|
||||
'location',
|
||||
'qty',
|
||||
'image',
|
||||
// These are *relationships* so we wouldn't normally include them in this array,
|
||||
// since they would normally create a `column not found` error,
|
||||
// BUT we account for them in the ordering switch down at the end of this method
|
||||
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
|
||||
'company',
|
||||
'location',
|
||||
'category',
|
||||
'supplier',
|
||||
'manufacturer',
|
||||
];
|
||||
|
||||
|
||||
$filter = [];
|
||||
|
||||
if ($request->filled('filter')) {
|
||||
$filter = json_decode($request->input('filter'), true);
|
||||
|
||||
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
|
||||
return in_array($key, $allowed_columns);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
}
|
||||
|
||||
if ((! is_null($filter)) && (count($filter)) > 0) {
|
||||
$consumables->ByFilter($filter);
|
||||
} elseif ($request->filled('search')) {
|
||||
$consumables->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$consumables->where('name', '=', $request->input('name'));
|
||||
}
|
||||
@@ -96,25 +139,6 @@ class ConsumablesController extends Controller
|
||||
$consumables = $consumables->OrderByCreatedBy($order);
|
||||
break;
|
||||
default:
|
||||
// This array is what determines which fields should be allowed to be sorted on ON the table itself.
|
||||
// These must match a column on the consumables table directly.
|
||||
$allowed_columns = [
|
||||
'id',
|
||||
'name',
|
||||
'order_number',
|
||||
'min_amt',
|
||||
'purchase_date',
|
||||
'purchase_cost',
|
||||
'company',
|
||||
'category',
|
||||
'model_number',
|
||||
'item_no',
|
||||
'manufacturer',
|
||||
'location',
|
||||
'qty',
|
||||
'image'
|
||||
];
|
||||
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
$consumables = $consumables->orderBy($sort, $order);
|
||||
break;
|
||||
@@ -228,11 +252,16 @@ class ConsumablesController extends Controller
|
||||
foreach ($consumable->consumableAssignments as $consumable_assignment) {
|
||||
$rows[] = [
|
||||
'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '',
|
||||
'name' => ($consumable_assignment->user) ? $consumable_assignment->user->present()->nameUrl() : 'Deleted User',
|
||||
'user' => ($consumable_assignment->user) ? [
|
||||
'id' => (int) $consumable_assignment->user->id,
|
||||
'name'=> e($consumable_assignment->user->display_name),
|
||||
] : null,
|
||||
'created_at' => Helper::getFormattedDateObject($consumable_assignment->created_at, 'datetime'),
|
||||
'note' => ($consumable_assignment->note) ? e($consumable_assignment->note) : null,
|
||||
'admin' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null, // legacy, so we don't change the shape of the response
|
||||
'created_by' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null,
|
||||
'created_by' => ($consumable_assignment->adminuser) ? [
|
||||
'id' => (int) $consumable_assignment->adminuser->id,
|
||||
'name'=> e($consumable_assignment->adminuser->display_name),
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ class ImportController extends Controller
|
||||
if (function_exists('iconv')) {
|
||||
$file_contents = $file->getContent(); //TODO - this *does* load the whole file in RAM, but we need that to be able to 'iconv' it?
|
||||
$encoding = $detector->getEncoding($file_contents);
|
||||
\Log::warning("Discovered encoding: $encoding in uploaded CSV");
|
||||
\Log::debug("Discovered encoding: $encoding in uploaded CSV");
|
||||
$reader = null;
|
||||
if (strcasecmp($encoding, 'UTF-8') != 0) {
|
||||
$transliterated = false;
|
||||
@@ -103,7 +103,7 @@ class ImportController extends Controller
|
||||
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
|
||||
|
||||
try {
|
||||
$import->header_row = $reader->fetchOne(0);
|
||||
$import->header_row = $reader->nth(0);
|
||||
} catch (JsonEncodingException $e) {
|
||||
return response()->json(
|
||||
Helper::formatStandardApiResponse(
|
||||
@@ -136,7 +136,7 @@ class ImportController extends Controller
|
||||
|
||||
try {
|
||||
// Grab the first row to display via ajax as the user picks fields
|
||||
$import->first_row = $reader->fetchOne(1);
|
||||
$import->first_row = $reader->nth(1);
|
||||
} catch (JsonEncodingException $e) {
|
||||
return response()->json(
|
||||
Helper::formatStandardApiResponse(
|
||||
@@ -195,7 +195,7 @@ class ImportController extends Controller
|
||||
// Run a backup immediately before processing
|
||||
if ($request->get('run-backup')) {
|
||||
Log::debug('Backup manually requested via importer');
|
||||
Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H:i:s')]);
|
||||
Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H-i-s')]);
|
||||
} else {
|
||||
Log::debug('NO BACKUP requested via importer');
|
||||
}
|
||||
|
||||
@@ -29,6 +29,15 @@ class LicenseSeatsController extends Controller
|
||||
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
|
||||
->where('license_seats.license_id', $licenseId);
|
||||
|
||||
if ($request->input('status') == 'available') {
|
||||
$seats->whereNull('license_seats.assigned_to');
|
||||
}
|
||||
|
||||
if ($request->input('status') == 'assigned') {
|
||||
$seats->ByAssigned();
|
||||
}
|
||||
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
if ($request->input('sort') == 'department') {
|
||||
@@ -119,7 +128,9 @@ class LicenseSeatsController extends Controller
|
||||
// nothing to update
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
||||
}
|
||||
|
||||
if( $touched && $licenseSeat->unreassignable_seat) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.checkout.unavailable')));
|
||||
}
|
||||
// the logging functions expect only one "target". if both asset and user are present in the request,
|
||||
// we simply let assets take precedence over users...
|
||||
if ($licenseSeat->isDirty('assigned_to')) {
|
||||
@@ -136,7 +147,11 @@ class LicenseSeatsController extends Controller
|
||||
if ($licenseSeat->save()) {
|
||||
|
||||
if ($is_checkin) {
|
||||
$licenseSeat->logCheckin($target, $request->input('notes'));
|
||||
if(!$licenseSeat->license->reassignable){
|
||||
$licenseSeat->unreassignable_seat = true;
|
||||
$licenseSeat->save();
|
||||
}
|
||||
$licenseSeat->logCheckin($target, $licenseSeat->notes);
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\LicensesTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\License;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -25,6 +26,15 @@ class LicensesController extends Controller
|
||||
$this->authorize('view', License::class);
|
||||
|
||||
$licenses = License::with('company', 'manufacturer', 'supplier','category', 'adminuser')->withCount('freeSeats as free_seats_count');
|
||||
$settings = Setting::getSettings();
|
||||
|
||||
if ($request->input('status')=='inactive') {
|
||||
$licenses->ExpiredLicenses();
|
||||
} elseif ($request->input('status')=='expiring') {
|
||||
$licenses->ExpiringLicenses($settings->alert_interval);
|
||||
} else {
|
||||
$licenses->ActiveLicenses();
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$licenses->where('licenses.company_id', '=', $request->input('company_id'));
|
||||
@@ -94,6 +104,8 @@ class LicensesController extends Controller
|
||||
$licenses->onlyTrashed();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $licenses->count()) ? $licenses->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
|
||||
@@ -37,10 +37,14 @@ class LocationsController extends Controller
|
||||
'address',
|
||||
'address2',
|
||||
'assets_count',
|
||||
'assets_count',
|
||||
'assigned_assets_count',
|
||||
'rtd_assets_count',
|
||||
'accessories_count',
|
||||
'assigned_accessories_count',
|
||||
'assigned_assets_count',
|
||||
'assigned_assets_count',
|
||||
'components_count',
|
||||
'consumables_count',
|
||||
'users_count',
|
||||
'children_count',
|
||||
'city',
|
||||
'country',
|
||||
'created_at',
|
||||
@@ -54,7 +58,6 @@ class LocationsController extends Controller
|
||||
'rtd_assets_count',
|
||||
'state',
|
||||
'updated_at',
|
||||
'users_count',
|
||||
'zip',
|
||||
'notes',
|
||||
];
|
||||
@@ -79,8 +82,9 @@ class LocationsController extends Controller
|
||||
'locations.currency',
|
||||
'locations.company_id',
|
||||
'locations.notes',
|
||||
'locations.created_by',
|
||||
'locations.deleted_at',
|
||||
])
|
||||
->withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('assignedAccessories as assigned_accessories_count')
|
||||
@@ -88,6 +92,8 @@ class LocationsController extends Controller
|
||||
->withCount('rtd_assets as rtd_assets_count')
|
||||
->withCount('children as children_count')
|
||||
->withCount('users as users_count')
|
||||
->withCount('consumables as consumables_count')
|
||||
->withCount('components as components_count')
|
||||
->with('adminuser');
|
||||
|
||||
// Only scope locations if the setting is enabled
|
||||
@@ -131,6 +137,14 @@ class LocationsController extends Controller
|
||||
$locations->where('locations.company_id', '=', $request->input('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('parent_id')) {
|
||||
$locations->where('locations.parent_id', '=', $request->input('parent_id'));
|
||||
}
|
||||
|
||||
if ($request->input('status') == 'deleted') {
|
||||
$locations->onlyTrashed();
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
@@ -224,8 +238,13 @@ class LocationsController extends Controller
|
||||
])
|
||||
->withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('assignedAccessories as assigned_accessories_count')
|
||||
->withCount('accessories as accessories_count')
|
||||
->withCount('rtd_assets as rtd_assets_count')
|
||||
->withCount('children as children_count')
|
||||
->withCount('users as users_count')
|
||||
->withCount('consumables as consumables_count')
|
||||
->withCount('components as components_count')
|
||||
->findOrFail($id);
|
||||
|
||||
return (new LocationsTransformer)->transformLocation($location);
|
||||
@@ -320,11 +339,15 @@ class LocationsController extends Controller
|
||||
{
|
||||
$this->authorize('delete', Location::class);
|
||||
$location = Location::withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('assignedAccessories as assigned_accessories_count')
|
||||
->withCount('accessories as accessories_count')
|
||||
->withCount('rtd_assets as rtd_assets_count')
|
||||
->withCount('children as children_count')
|
||||
->withCount('users as users_count')
|
||||
->withCount('accessories as accessories_count')
|
||||
->withCount('consumables as consumables_count')
|
||||
->withCount('components as components_count')
|
||||
->findOrFail($id);
|
||||
|
||||
if (! $location->isDeletable()) {
|
||||
|
||||
@@ -4,11 +4,11 @@ namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\AssetMaintenancesTransformer;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use App\Http\Transformers\MaintenancesTransformer;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetMaintenance;
|
||||
use App\Models\Maintenance;
|
||||
use App\Models\Company;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
@@ -18,13 +18,13 @@ use Illuminate\Http\JsonResponse;
|
||||
*
|
||||
* @version v2.0
|
||||
*/
|
||||
class AssetMaintenancesController extends Controller
|
||||
class MaintenancesController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Generates the JSON response for asset maintenances listing view.
|
||||
*
|
||||
* @see AssetMaintenancesController::getIndex() method that generates view
|
||||
* @see MaintenancesController::getIndex() method that generates view
|
||||
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||
* @version v1.0
|
||||
* @since [v1.8]
|
||||
@@ -33,7 +33,7 @@ class AssetMaintenancesController extends Controller
|
||||
{
|
||||
$this->authorize('view', Asset::class);
|
||||
|
||||
$maintenances = AssetMaintenance::select('asset_maintenances.*')
|
||||
$maintenances = Maintenance::select('maintenances.*')
|
||||
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'adminuser');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
@@ -45,11 +45,15 @@ class AssetMaintenancesController extends Controller
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
|
||||
$maintenances->where('maintenances.supplier_id', '=', $request->input('supplier_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('created_by')) {
|
||||
$maintenances->where('asset_maintenances.created_by', '=', $request->input('created_by'));
|
||||
$maintenances->where('maintenances.created_by', '=', $request->input('created_by'));
|
||||
}
|
||||
|
||||
if ($request->filled('url')) {
|
||||
$maintenances->where('maintenances.url', '=', $request->input('url'));
|
||||
}
|
||||
|
||||
if ($request->filled('asset_maintenance_type')) {
|
||||
@@ -63,7 +67,7 @@ class AssetMaintenancesController extends Controller
|
||||
|
||||
$allowed_columns = [
|
||||
'id',
|
||||
'title',
|
||||
'name',
|
||||
'asset_maintenance_time',
|
||||
'asset_maintenance_type',
|
||||
'cost',
|
||||
@@ -75,6 +79,7 @@ class AssetMaintenancesController extends Controller
|
||||
'serial',
|
||||
'created_by',
|
||||
'supplier',
|
||||
'location',
|
||||
'is_warranty',
|
||||
'status_label',
|
||||
];
|
||||
@@ -98,6 +103,9 @@ class AssetMaintenancesController extends Controller
|
||||
case 'serial':
|
||||
$maintenances = $maintenances->OrderByAssetSerial($order);
|
||||
break;
|
||||
case 'location':
|
||||
$maintenances = $maintenances->OrderLocationName($order);
|
||||
break;
|
||||
case 'status_label':
|
||||
$maintenances = $maintenances->OrderStatusName($order);
|
||||
break;
|
||||
@@ -108,7 +116,7 @@ class AssetMaintenancesController extends Controller
|
||||
|
||||
$total = $maintenances->count();
|
||||
$maintenances = $maintenances->skip($offset)->take($limit)->get();
|
||||
return (new AssetMaintenancesTransformer())->transformAssetMaintenances($maintenances, $total);
|
||||
return (new MaintenancesTransformer())->transformMaintenances($maintenances, $total);
|
||||
|
||||
|
||||
}
|
||||
@@ -117,22 +125,23 @@ class AssetMaintenancesController extends Controller
|
||||
/**
|
||||
* Validates and stores the new asset maintenance
|
||||
*
|
||||
* @see AssetMaintenancesController::getCreate() method for the form
|
||||
* @see MaintenancesController::getCreate() method for the form
|
||||
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||
* @version v1.0
|
||||
* @since [v1.8]
|
||||
*/
|
||||
public function store(Request $request) : JsonResponse | array
|
||||
public function store(ImageUploadRequest $request) : JsonResponse | array
|
||||
{
|
||||
$this->authorize('update', Asset::class);
|
||||
|
||||
// create a new model instance
|
||||
$maintenance = new AssetMaintenance();
|
||||
$maintenance = new Maintenance();
|
||||
$maintenance->fill($request->all());
|
||||
$maintenance->created_by = auth()->id();
|
||||
|
||||
$maintenance = $request->handleImages($maintenance);
|
||||
// Was the asset maintenance created?
|
||||
if ($maintenance->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.create.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.create.success')));
|
||||
|
||||
}
|
||||
|
||||
@@ -153,11 +162,11 @@ class AssetMaintenancesController extends Controller
|
||||
{
|
||||
$this->authorize('update', Asset::class);
|
||||
|
||||
if ($maintenance = AssetMaintenance::with('asset')->find($id)) {
|
||||
if ($maintenance = Maintenance::with('asset')->find($id)) {
|
||||
|
||||
// Can this user manage this asset?
|
||||
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
|
||||
}
|
||||
|
||||
// The asset this miantenance is attached to is not valid or has been deleted
|
||||
@@ -168,13 +177,13 @@ class AssetMaintenancesController extends Controller
|
||||
$maintenance->fill($request->all());
|
||||
|
||||
if ($maintenance->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.edit.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.edit.success')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id])));
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $id])));
|
||||
|
||||
}
|
||||
|
||||
@@ -182,20 +191,20 @@ class AssetMaintenancesController extends Controller
|
||||
* Delete an asset maintenance
|
||||
*
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
* @param int $assetMaintenanceId
|
||||
* @param int $maintenanceId
|
||||
* @version v1.0
|
||||
* @since [v4.0]
|
||||
*/
|
||||
public function destroy($assetMaintenanceId) : JsonResponse | array
|
||||
public function destroy($maintenanceId) : JsonResponse | array
|
||||
{
|
||||
$this->authorize('update', Asset::class);
|
||||
// Check if the asset maintenance exists
|
||||
|
||||
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
|
||||
$maintenance = Maintenance::findOrFail($maintenanceId);
|
||||
|
||||
$assetMaintenance->delete();
|
||||
$maintenance->delete();
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.delete.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.delete.success')));
|
||||
|
||||
|
||||
}
|
||||
@@ -204,19 +213,19 @@ class AssetMaintenancesController extends Controller
|
||||
* View an asset maintenance
|
||||
*
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
* @param int $assetMaintenanceId
|
||||
* @param int $maintenanceId
|
||||
* @version v1.0
|
||||
* @since [v4.0]
|
||||
*/
|
||||
public function show($assetMaintenanceId) : JsonResponse | array
|
||||
public function show($maintenanceId) : JsonResponse | array
|
||||
{
|
||||
$this->authorize('view', Asset::class);
|
||||
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
|
||||
if (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
|
||||
$maintenance = Maintenance::findOrFail($maintenanceId);
|
||||
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot view a maintenance for that asset'));
|
||||
}
|
||||
|
||||
return (new AssetMaintenancesTransformer())->transformAssetMaintenance($assetMaintenance);
|
||||
return (new MaintenancesTransformer())->transformMaintenance($maintenance);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Manufacturers\DeleteManufacturerAction;
|
||||
use App\Exceptions\ItemStillHasAccessories;
|
||||
use App\Exceptions\ItemStillHasAssets;
|
||||
use App\Exceptions\ItemStillHasChildren;
|
||||
use App\Exceptions\ItemStillHasComponents;
|
||||
use App\Exceptions\ItemStillHasConsumables;
|
||||
use App\Exceptions\ItemStillHasLicenses;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\ManufacturersTransformer;
|
||||
@@ -184,19 +191,19 @@ class ManufacturersController extends Controller
|
||||
* @since [v4.0]
|
||||
* @param int $id
|
||||
*/
|
||||
public function destroy($id) : JsonResponse
|
||||
public function destroy(Manufacturer $manufacturer): JsonResponse
|
||||
{
|
||||
$this->authorize('delete', Manufacturer::class);
|
||||
$manufacturer = Manufacturer::findOrFail($id);
|
||||
$this->authorize('delete', $manufacturer);
|
||||
|
||||
if ($manufacturer->isDeletable()) {
|
||||
$manufacturer->delete();
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/manufacturers/message.delete.success')));
|
||||
try {
|
||||
DeleteManufacturerAction::run($manufacturer);
|
||||
} catch (ItemStillHasChildren $e) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.general_assoc_warning', ['item' => trans('general.manufacturer')])));
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/manufacturers/message.assoc_users')));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/manufacturers/message.delete.success')));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
95
app/Http/Controllers/Api/NotesController.php
Normal file
95
app/Http/Controllers/Api/NotesController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* This class controls all API actions related to notes for
|
||||
* the Snipe-IT Asset Management application.
|
||||
*/
|
||||
class NotesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Retrieve a list of manual notes (action logs) for a given asset.
|
||||
*
|
||||
* Checks authorization to view assets, attempts to find the asset by ID,
|
||||
* and fetches related action log entries of type 'note added', including
|
||||
* user information for each note. Returns a JSON response with the notes or errors.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request The incoming HTTP request.
|
||||
* @param Asset $asset The ID of the asset whose notes to retrieve.
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function index(Asset $asset): JsonResponse
|
||||
{
|
||||
$this->authorize('view', $asset);
|
||||
|
||||
// Get the manual notes for the asset
|
||||
$notes = ActionLog::with('user:id,username')
|
||||
->where('item_type', Asset::class)
|
||||
->where('item_id', $asset->id)
|
||||
->where('action_type', 'note added')
|
||||
->orderBy('created_at', 'desc')
|
||||
->get(['id', 'created_at', 'note', 'created_by', 'item_id', 'item_type', 'action_type', 'target_id', 'target_type']);
|
||||
|
||||
$notesArray = $notes->map(function ($note) {
|
||||
return [
|
||||
'id' => $note->id,
|
||||
'created_at' => $note->created_at,
|
||||
'note' => $note->note,
|
||||
'created_by' => $note->created_by,
|
||||
'username' => $note->user?->username, // adding the username
|
||||
'item_id' => $note->item_id,
|
||||
'item_type' => $note->item_type,
|
||||
'action_type' => $note->action_type,
|
||||
];
|
||||
});
|
||||
|
||||
// Return a success response
|
||||
return response()->json(Helper::formatStandardApiResponse('success', ['notes' => $notesArray, 'asset_id' => $asset->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a manual note on a specified asset and log the action.
|
||||
*
|
||||
* Checks authorization for updating assets, validates the presence of the 'note',
|
||||
* attempts to find the asset by ID, and creates a new ActionLog entry if successful.
|
||||
* Returns JSON responses indicating success or failure with appropriate HTTP status codes.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request The incoming HTTP request containing the 'note'.
|
||||
* @param Asset $asset The ID of the asset to attach the note to.
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function store(Request $request, Asset $asset): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $asset);
|
||||
|
||||
if ($request->input('note', '') == '') {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('validation.required', ['attribute' => 'note'])), 422);
|
||||
}
|
||||
|
||||
// Create the note
|
||||
$logaction = new ActionLog();
|
||||
$logaction->item_type = get_class($asset);
|
||||
$logaction->created_by = Auth::id();
|
||||
$logaction->item_id = $asset->id;
|
||||
$logaction->note = $request->input('note', '');
|
||||
|
||||
if ($logaction->logaction('note added')) {
|
||||
// Return a success response
|
||||
return response()->json(Helper::formatStandardApiResponse('success', ['note' => $logaction->note, 'item_id' => $asset->id], trans('general.note_added')));
|
||||
}
|
||||
|
||||
// Return an error response if something went wrong
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Something went wrong'), 500);
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,19 @@ namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\ProfileTransformer;
|
||||
use App\Models\CheckoutRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Passport\TokenRepository;
|
||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use App\Models\CustomField;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
@@ -65,7 +69,7 @@ class ProfileController extends Controller
|
||||
if ($checkoutRequest && $checkoutRequest->itemRequested()) {
|
||||
$assets = [
|
||||
'image' => e($checkoutRequest->itemRequested()->present()->getImageUrl()),
|
||||
'name' => e($checkoutRequest->itemRequested()->present()->name()),
|
||||
'name' => e($checkoutRequest->itemRequested()->display_name),
|
||||
'type' => e($checkoutRequest->itemType()),
|
||||
'qty' => (int) $checkoutRequest->quantity,
|
||||
'location' => ($checkoutRequest->location()) ? e($checkoutRequest->location()->name) : null,
|
||||
@@ -167,6 +171,22 @@ class ProfileController extends Controller
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the EULAs accepted by the user.
|
||||
*
|
||||
* @param \App\Http\Transformers\ActionlogsTransformer $transformer
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*@since [v8.1.16]
|
||||
* @author [Godfrey Martinez] [<gmartinez@grokability.com>]
|
||||
*/
|
||||
public function eulas(ProfileTransformer $transformer)
|
||||
{
|
||||
// Only return this user's EULAs
|
||||
$eulas = auth()->user()->eulas;
|
||||
return response()->json(
|
||||
$transformer->transformFiles($eulas, $eulas->count())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Transformers\DatatablesTransformer;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -51,10 +50,22 @@ class SettingsController extends Controller
|
||||
})->slice(0, 10)->map(function ($item) use ($settings) {
|
||||
return (object) [
|
||||
'username' => $item[$settings['ldap_username_field']][0] ?? null,
|
||||
'display_name' => $item[$settings['ldap_display_name']][0] ?? null,
|
||||
'employee_number' => $item[$settings['ldap_emp_num']][0] ?? null,
|
||||
'lastname' => $item[$settings['ldap_lname_field']][0] ?? null,
|
||||
'firstname' => $item[$settings['ldap_fname_field']][0] ?? null,
|
||||
'email' => $item[$settings['ldap_email']][0] ?? null,
|
||||
'phone' => $item[$settings['ldap_phone_field']][0] ?? null,
|
||||
'mobile' => $item[$settings['ldap_mobile']][0] ?? null,
|
||||
'jobtitle' => $item[$settings['ldap_jobtitle']][0] ?? null,
|
||||
'department' => $item[$settings['ldap_department']][0] ?? null,
|
||||
'manager' => $item[$settings['ldap_manager']][0] ?? null,
|
||||
'address' => $item[$settings['ldap_address']][0] ?? null,
|
||||
'city' => $item[$settings['ldap_city']][0] ?? null,
|
||||
'state' => $item[$settings['ldap_state']][0] ?? null,
|
||||
'zip' => $item[$settings['ldap_zip']][0] ?? null,
|
||||
'country' => $item[$settings['ldap_country']][0] ?? null,
|
||||
'location' => $item[$settings['ldap_location']][0] ?? null,
|
||||
];
|
||||
});
|
||||
if ($users->count() > 0) {
|
||||
@@ -78,7 +89,7 @@ class SettingsController extends Controller
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::debug('Connection failed but we cannot debug it any further on our end.');
|
||||
return response()->json(['message' => $e->getMessage()], 500);
|
||||
return response()->json(['message' => $e->getMessage()], 400);
|
||||
}
|
||||
|
||||
|
||||
@@ -150,8 +161,11 @@ class SettingsController extends Controller
|
||||
if (!config('app.lock_passwords')) {
|
||||
try {
|
||||
Notification::send(Setting::first(), new MailTest());
|
||||
Log::debug('Attempting to sending to '.config('mail.reply_to.address'));
|
||||
return response()->json(['message' => 'Mail sent to '.config('mail.reply_to.address')], 200);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Mail sent error using '.config('mail.reply_to.address') .': '. $e->getMessage());
|
||||
Log::debug($e);
|
||||
return response()->json(['message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Suppliers\DestroySupplierAction;
|
||||
use App\Exceptions\ItemStillHasAccessories;
|
||||
use App\Exceptions\ItemStillHasComponents;
|
||||
use App\Exceptions\ItemStillHasConsumables;
|
||||
use App\Exceptions\ItemStillHasMaintenances;
|
||||
use App\Exceptions\ItemStillHasAssets;
|
||||
use App\Exceptions\ItemStillHasLicenses;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
@@ -191,27 +198,40 @@ class SuppliersController extends Controller
|
||||
* @since [v4.0]
|
||||
* @param int $id
|
||||
*/
|
||||
public function destroy($id) : JsonResponse
|
||||
public function destroy(Supplier $supplier): JsonResponse
|
||||
{
|
||||
$this->authorize('delete', Supplier::class);
|
||||
$supplier = Supplier::with('asset_maintenances', 'assets', 'licenses')->withCount('asset_maintenances as asset_maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id);
|
||||
$this->authorize('delete', $supplier);
|
||||
|
||||
|
||||
if ($supplier->assets_count > 0) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count])));
|
||||
try {
|
||||
DestroySupplierAction::run(supplier: $supplier);
|
||||
} catch (ItemStillHasAssets $e) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_assets', [
|
||||
'asset_count' => (int) $supplier->assets_count, 'item' => trans('general.supplier')
|
||||
])));
|
||||
} catch (ItemStillHasMaintenances $e) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_maintenances', [
|
||||
'asset_maintenances_count' => $supplier->asset_maintenances_count, 'item' => trans('general.supplier')
|
||||
])));
|
||||
} catch (ItemStillHasLicenses $e) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_licenses', [
|
||||
'licenses_count' => (int) $supplier->licenses_count, 'item' => trans('general.supplier')
|
||||
])));
|
||||
} catch (ItemStillHasAccessories $e) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_accessories', [
|
||||
'accessories_count' => (int) $supplier->accessories_count, 'item' => trans('general.supplier')
|
||||
])));
|
||||
} catch (ItemStillHasConsumables $e) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_consumables', [
|
||||
'consumables_count' => (int) $supplier->consumables_count, 'item' => trans('general.supplier')
|
||||
])));
|
||||
} catch (ItemStillHasComponents $e) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_components', [
|
||||
'components_count' => (int) $supplier->components_count, 'item' => trans('general.supplier')
|
||||
])));
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
|
||||
}
|
||||
|
||||
if ($supplier->asset_maintenances_count > 0) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count])));
|
||||
}
|
||||
|
||||
if ($supplier->licenses_count > 0) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_licenses', ['licenses_count' => (int) $supplier->licenses_count])));
|
||||
}
|
||||
|
||||
$supplier->delete();
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/suppliers/message.delete.success')));
|
||||
}
|
||||
|
||||
|
||||
220
app/Http/Controllers/Api/UploadedFilesController.php
Normal file
220
app/Http/Controllers/Api/UploadedFilesController.php
Normal file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Http\Transformers\UploadedFilesTransformer;
|
||||
use App\Models\Actionlog;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
|
||||
class UploadedFilesController extends Controller
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* List files for an object
|
||||
*
|
||||
* @param \App\Http\Requests\UploadFileRequest $request
|
||||
* @param string $object_type the type of object to upload the file to
|
||||
* @param int $id the ID of the object to list files for
|
||||
* @since [v8.1.17]
|
||||
* @author [A. Gianotto <snipe@snipe.net>]
|
||||
*/
|
||||
public function index(Request $request, $object_type, $id) : JsonResponse | array
|
||||
{
|
||||
|
||||
// Check the permissions to make sure the user can view the object
|
||||
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
|
||||
$this->authorize('view', $object);
|
||||
|
||||
if (!$object) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
|
||||
}
|
||||
|
||||
// Columns allowed for sorting
|
||||
$allowed_columns =
|
||||
[
|
||||
'id',
|
||||
'filename',
|
||||
'action_type',
|
||||
'action_date',
|
||||
'note',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
|
||||
$uploads = self::$map_object_type[$object_type]::withTrashed()->find($id)->uploads()
|
||||
->with('adminuser');
|
||||
|
||||
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset'));
|
||||
$limit = app('api_limit_value');
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
|
||||
// Text search on action_logs fields
|
||||
// We could use the normal Actionlogs text scope, but it's a very heavy query since it's searching across all relations
|
||||
// and we generally won't need that here
|
||||
if ($request->filled('search')) {
|
||||
|
||||
$uploads->where(
|
||||
function ($query) use ($request) {
|
||||
$query->where('filename', 'LIKE', '%' . $request->input('search') . '%')
|
||||
->orWhere('note', 'LIKE', '%' . $request->input('search') . '%');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$total = $uploads->count();
|
||||
$uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get();
|
||||
|
||||
return (new UploadedFilesTransformer())->transformFiles($uploads, $total);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Accepts a POST to upload a file to the server.
|
||||
*
|
||||
* @param \App\Http\Requests\UploadFileRequest $request
|
||||
* @param string $object_type the type of object to upload the file to
|
||||
* @param int $id the ID of the object to store so we can check permisisons
|
||||
* @since [v8.1.17]
|
||||
* @author [A. Gianotto <snipe@snipe.net>]
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $object_type, $id) : JsonResponse
|
||||
{
|
||||
|
||||
// Check the permissions to make sure the user can view the object
|
||||
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
|
||||
$this->authorize('view', $object);
|
||||
|
||||
if (!$object) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
|
||||
}
|
||||
|
||||
// If the file storage directory doesn't exist, create it
|
||||
if (! Storage::exists(self::$map_storage_path[$object_type])) {
|
||||
Storage::makeDirectory(self::$map_storage_path[$object_type], 775);
|
||||
}
|
||||
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
// Loop over the attached files and add them to the object
|
||||
foreach ($request->file('file') as $file) {
|
||||
$file_name = $request->handleFile(self::$map_storage_path[$object_type], self::$map_file_prefix[$object_type].'-'.$object->id, $file);
|
||||
$files[] = $file_name;
|
||||
$object->logUpload($file_name, $request->get('notes'));
|
||||
}
|
||||
|
||||
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')
|
||||
->where('item_type', '=', self::$map_object_type[$object_type])
|
||||
->where('item_id', '=', $id)->whereIn('filename', $files)
|
||||
->get();
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', (new UploadedFilesTransformer())->transformFiles($files, count($files)), trans_choice('general.file_upload_status.upload.success', count($files))));
|
||||
}
|
||||
|
||||
// No files were submitted
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.nofiles')));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check for permissions and display the file.
|
||||
*
|
||||
* @param \App\Http\Requests\UploadFileRequest $request
|
||||
* @param string $object_type the type of object to upload the file to
|
||||
* @param int $id the ID of the object to delete from so we can check permisisons
|
||||
* @param $file_id the ID of the file to delete from the action_logs table
|
||||
* @since [v8.1.17]
|
||||
* @author [A. Gianotto <snipe@snipe.net>]
|
||||
*/
|
||||
public function show($object_type, $id, $file_id) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
|
||||
{
|
||||
// Check the permissions to make sure the user can view the object
|
||||
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
|
||||
$this->authorize('view', $object);
|
||||
|
||||
if (!$object) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
|
||||
}
|
||||
|
||||
|
||||
// Check that the file being requested exists for the object
|
||||
if (! $log = Actionlog::whereNotNull('filename')->where('item_type', self::$map_object_type[$object_type])->where('item_id', $object->id)->find($file_id)
|
||||
) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_id')), 200);
|
||||
}
|
||||
|
||||
|
||||
if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.file_not_found'), 200));
|
||||
}
|
||||
|
||||
if (request('inline') == 'true') {
|
||||
$headers = [
|
||||
'Content-Disposition' => 'inline',
|
||||
];
|
||||
return Storage::download(self::$map_storage_path[$object_type].'/'.$log->filename, $log->filename, $headers);
|
||||
}
|
||||
|
||||
return StorageHelper::downloader(self::$map_storage_path[$object_type].'/'.$log->filename);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the associated file
|
||||
*
|
||||
* @param \App\Http\Requests\UploadFileRequest $request
|
||||
* @param string $object_type the type of object to upload the file to
|
||||
* @param int $id the ID of the object to delete from so we can check permisisons
|
||||
* @param $file_id the ID of the file to delete from the action_logs table
|
||||
* @since [v8.1.17]
|
||||
* @author [A. Gianotto <snipe@snipe.net>]
|
||||
*/
|
||||
public function destroy($object_type, $id, $file_id) : JsonResponse
|
||||
{
|
||||
|
||||
// Check the permissions to make sure the user can view the object
|
||||
$object = self::$map_object_type[$object_type]::withTrashed()->find($id);
|
||||
$this->authorize('update', self::$map_object_type[$object_type]);
|
||||
|
||||
if (!$object) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
|
||||
}
|
||||
|
||||
|
||||
// Check for the file
|
||||
$log = Actionlog::query()
|
||||
->where('id', $file_id)
|
||||
->where('action_type', 'uploaded')
|
||||
->where('item_type', self::$map_object_type[$object_type])
|
||||
->where('item_id', $object->id)
|
||||
->first();
|
||||
|
||||
if ($log) {
|
||||
// Check the file actually exists, and delete it
|
||||
if (Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
|
||||
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
|
||||
}
|
||||
// Delete the record of the file
|
||||
if ($log->logUploadDelete($object, $log->filename)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// The file doesn't seem to really exist, so report an error
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,12 @@ use App\Models\Consumable;
|
||||
use App\Models\License;
|
||||
use App\Models\User;
|
||||
use App\Notifications\CurrentInventory;
|
||||
use App\Notifications\WelcomeNotification;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -62,12 +65,14 @@ class UsersController extends Controller
|
||||
'users.jobtitle',
|
||||
'users.last_login',
|
||||
'users.last_name',
|
||||
'users.display_name',
|
||||
'users.locale',
|
||||
'users.location_id',
|
||||
'users.manager_id',
|
||||
'users.notes',
|
||||
'users.permissions',
|
||||
'users.phone',
|
||||
'users.mobile',
|
||||
'users.state',
|
||||
'users.two_factor_enrolled',
|
||||
'users.two_factor_optin',
|
||||
@@ -81,7 +86,12 @@ class UsersController extends Controller
|
||||
'users.autoassign_licenses',
|
||||
'users.website',
|
||||
|
||||
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations', 'eulas')
|
||||
])->with('manager')
|
||||
->with('groups')
|
||||
->with('userloc')
|
||||
->with('company')
|
||||
->with('department')
|
||||
->with('createdBy')
|
||||
->withCount([
|
||||
'assets as assets_count' => function(Builder $query) {
|
||||
$query->withoutTrashed();
|
||||
@@ -93,19 +103,101 @@ class UsersController extends Controller
|
||||
'managedLocations as manages_locations_count'
|
||||
]);
|
||||
|
||||
$allowed_columns =
|
||||
[
|
||||
'last_name',
|
||||
'first_name',
|
||||
'display_name',
|
||||
'email',
|
||||
'jobtitle',
|
||||
'username',
|
||||
'employee_num',
|
||||
'groups',
|
||||
'activated',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'two_factor_enrolled',
|
||||
'two_factor_optin',
|
||||
'last_login',
|
||||
'assets_count',
|
||||
'licenses_count',
|
||||
'consumables_count',
|
||||
'accessories_count',
|
||||
'manages_users_count',
|
||||
'manages_locations_count',
|
||||
'phone',
|
||||
'mobile',
|
||||
'address',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
'zip',
|
||||
'id',
|
||||
'ldap_import',
|
||||
'two_factor_optin',
|
||||
'two_factor_enrolled',
|
||||
'remote',
|
||||
'vip',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'autoassign_licenses',
|
||||
'website',
|
||||
'locale',
|
||||
'notes',
|
||||
'employee_num',
|
||||
|
||||
if ($request->filled('search') != '') {
|
||||
$users = $users->TextSearch($request->input('search'));
|
||||
// These are *relationships* so we wouldn't normally include them in this array,
|
||||
// since they would normally create a `column not found` error,
|
||||
// BUT we account for them in the ordering switch down at the end of this method
|
||||
// DO NOT ADD ANYTHING TO THIS LIST WITHOUT CHECKING THE ORDERING SWITCH BELOW!
|
||||
'company',
|
||||
'location',
|
||||
'department',
|
||||
'manager',
|
||||
'created_by',
|
||||
|
||||
];
|
||||
|
||||
$filter = [];
|
||||
|
||||
if ($request->filled('filter')) {
|
||||
$filter = json_decode($request->input('filter'), true);
|
||||
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
|
||||
return in_array($key, $allowed_columns);
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
}
|
||||
|
||||
if ((! is_null($filter)) && (count($filter)) > 0) {
|
||||
$users->ByFilter($filter);
|
||||
} elseif ($request->filled('search')) {
|
||||
$users->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('activated')) {
|
||||
$users = $users->where('users.activated', '=', $request->input('activated'));
|
||||
}
|
||||
|
||||
if ($request->input('admins') == 'true') {
|
||||
$users = $users->OnlyAdminsAndSuperAdmins();
|
||||
}
|
||||
|
||||
if ($request->input('superadmins') == 'true') {
|
||||
$users = $users->OnlySuperAdmins();
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$users = $users->where('users.company_id', '=', $request->input('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('phone')) {
|
||||
$users = $users->where('users.phone', '=', $request->input('phone'));
|
||||
}
|
||||
|
||||
if ($request->filled('mobile')) {
|
||||
$users = $users->where('users.mobile', '=', $request->input('mobile'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$users = $users->where('users.location_id', '=', $request->input('location_id'));
|
||||
}
|
||||
@@ -130,6 +222,10 @@ class UsersController extends Controller
|
||||
$users = $users->where('users.last_name', '=', $request->input('last_name'));
|
||||
}
|
||||
|
||||
if ($request->filled('display_name')) {
|
||||
$users = $users->where('users.display_name', '=', $request->input('display_name'));
|
||||
}
|
||||
|
||||
if ($request->filled('employee_num')) {
|
||||
$users = $users->where('users.employee_num', '=', $request->input('employee_num'));
|
||||
}
|
||||
@@ -256,47 +352,6 @@ class UsersController extends Controller
|
||||
$users->orderBy('first_name', $order);
|
||||
break;
|
||||
default:
|
||||
$allowed_columns =
|
||||
[
|
||||
'last_name',
|
||||
'first_name',
|
||||
'email',
|
||||
'jobtitle',
|
||||
'username',
|
||||
'employee_num',
|
||||
'groups',
|
||||
'activated',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'two_factor_enrolled',
|
||||
'two_factor_optin',
|
||||
'last_login',
|
||||
'assets_count',
|
||||
'licenses_count',
|
||||
'consumables_count',
|
||||
'accessories_count',
|
||||
'manages_users_count',
|
||||
'manages_locations_count',
|
||||
'phone',
|
||||
'address',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
'zip',
|
||||
'id',
|
||||
'ldap_import',
|
||||
'two_factor_optin',
|
||||
'two_factor_enrolled',
|
||||
'remote',
|
||||
'vip',
|
||||
'start_date',
|
||||
'end_date',
|
||||
'autoassign_licenses',
|
||||
'website',
|
||||
'locale',
|
||||
'notes',
|
||||
];
|
||||
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'first_name';
|
||||
$users = $users->orderBy($sort, $order);
|
||||
break;
|
||||
@@ -330,6 +385,7 @@ class UsersController extends Controller
|
||||
'users.employee_num',
|
||||
'users.first_name',
|
||||
'users.last_name',
|
||||
'users.display_name',
|
||||
'users.gravatar',
|
||||
'users.avatar',
|
||||
'users.email',
|
||||
@@ -340,20 +396,17 @@ class UsersController extends Controller
|
||||
$users = $users->where(function ($query) use ($request) {
|
||||
$query->SimpleNameSearch($request->get('search'))
|
||||
->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
|
||||
->orWhere('display_name', 'LIKE', '%'.$request->get('search').'%')
|
||||
->orWhere('email', 'LIKE', '%'.$request->get('search').'%')
|
||||
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
|
||||
});
|
||||
}
|
||||
|
||||
$users = $users->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
|
||||
$users = $users->orderBy('display_name', 'asc')->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
|
||||
$users = $users->paginate(50);
|
||||
|
||||
foreach ($users as $user) {
|
||||
$name_str = '';
|
||||
if ($user->last_name != '') {
|
||||
$name_str .= $user->last_name.', ';
|
||||
}
|
||||
$name_str .= $user->first_name;
|
||||
$name_str = $user->display_name;
|
||||
|
||||
if ($user->username != '') {
|
||||
$name_str .= ' ('.$user->username.')';
|
||||
@@ -405,9 +458,20 @@ class UsersController extends Controller
|
||||
$user->password = $user->noPassword();
|
||||
}
|
||||
|
||||
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
|
||||
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
|
||||
|
||||
if ($user->save()) {
|
||||
|
||||
if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
|
||||
|
||||
try {
|
||||
$user->notify(new WelcomeNotification($user));
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($request->filled('groups')) {
|
||||
$user->groups()->sync($request->input('groups'));
|
||||
} else {
|
||||
@@ -475,8 +539,29 @@ class UsersController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
|
||||
}
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$user->password = bcrypt($request->input('password'));
|
||||
// check for permissions related fields and pull them out if the current user cannot edit them
|
||||
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$user->password = bcrypt($request->input('password'));
|
||||
}
|
||||
|
||||
if ($request->filled('username')) {
|
||||
$user->username = $request->input('username');
|
||||
}
|
||||
|
||||
if ($request->filled('display_name')) {
|
||||
$user->display_name = $request->input('display_name');
|
||||
}
|
||||
|
||||
if ($request->filled('email')) {
|
||||
$user->email = $request->input('email');
|
||||
}
|
||||
|
||||
if ($request->filled('activated')) {
|
||||
$user->activated = $request->input('activated');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// We need to use has() instead of filled()
|
||||
@@ -498,7 +583,7 @@ class UsersController extends Controller
|
||||
Asset::where('assigned_type', User::class)
|
||||
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
|
||||
}
|
||||
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
|
||||
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
|
||||
|
||||
if ($user->save()) {
|
||||
// Check if the request has groups passed and has a value, AND that the user us a superuser
|
||||
@@ -677,7 +762,6 @@ class UsersController extends Controller
|
||||
$this->authorize('view', License::class);
|
||||
|
||||
if ($user = User::where('id', $id)->withTrashed()->first()) {
|
||||
$this->authorize('update', $user);
|
||||
$licenses = $user->licenses()->get();
|
||||
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
|
||||
}
|
||||
@@ -748,7 +832,7 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function eulas(User $user, ActionlogsTransformer $transformer)
|
||||
{
|
||||
$this->authorize('view', Asset::class);
|
||||
$this->authorize('view', User::class);
|
||||
|
||||
$eulas = $user->eulas;
|
||||
return response()->json(
|
||||
@@ -792,4 +876,37 @@ class UsersController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Run the LDAP sync command to import users from LDAP via API.
|
||||
*
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
* @since 8.2.2
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function syncLdapUsers(Request $request)
|
||||
{
|
||||
$this->authorize('update', User::class);
|
||||
// Call Artisan LDAP import command.
|
||||
|
||||
Artisan::call('snipeit:ldap-sync', ['--location_id' => $request->input('location_id'), '--json_summary' => true]);
|
||||
|
||||
// Collect and parse JSON summary.
|
||||
$ldap_results_json = Artisan::output();
|
||||
$ldap_results = json_decode($ldap_results_json, true);
|
||||
|
||||
if (!$ldap_results) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null,trans('general.no_results')), 200);
|
||||
}
|
||||
|
||||
// Direct user to appropriate status page.
|
||||
if ($ldap_results['error']) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $ldap_results['error_message']), 200);
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, $ldap_results['summary']), 200);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,12 +82,26 @@ class AssetModelsController extends Controller
|
||||
$model->notes = $request->input('notes');
|
||||
$model->created_by = auth()->id();
|
||||
$model->requestable = $request->has('requestable');
|
||||
$model->require_serial = $request->input('require_serial', 0);
|
||||
|
||||
if ($request->input('fieldset_id') != '') {
|
||||
$model->fieldset_id = $request->input('fieldset_id');
|
||||
}
|
||||
|
||||
$model = $request->handleImages($model);
|
||||
if ($request->has('use_cloned_image')) {
|
||||
$cloned_model_img = AssetModel::select('image')->find($request->input('clone_image_from_id'));
|
||||
if ($cloned_model_img) {
|
||||
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
|
||||
$new_image = 'models/'.$new_image_name;
|
||||
Storage::disk('public')->copy('models/'.$cloned_model_img->image, $new_image);
|
||||
$model->image = $new_image_name;
|
||||
}
|
||||
|
||||
} else {
|
||||
$model = $request->handleImages($model);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ($model->save()) {
|
||||
if ($this->shouldAddDefaultValues($request->input())) {
|
||||
@@ -142,7 +156,7 @@ class AssetModelsController extends Controller
|
||||
$model->category_id = $request->input('category_id');
|
||||
$model->notes = $request->input('notes');
|
||||
$model->requestable = $request->input('requestable', '0');
|
||||
|
||||
$model->require_serial = $request->input('require_serial', 0);
|
||||
$model->fieldset_id = $request->input('fieldset_id');
|
||||
|
||||
if ($model->save()) {
|
||||
@@ -271,7 +285,7 @@ class AssetModelsController extends Controller
|
||||
->with('depreciation_list', Helper::depreciationList())
|
||||
->with('item', $model)
|
||||
->with('model_id', $model->id)
|
||||
->with('clone_model', $cloned_model);
|
||||
->with('cloned_model', $cloned_model);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use \Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class AssetModelsFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Upload a file to the server.
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
* @param int $modelId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*@since [v1.0]
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $modelId = null) : RedirectResponse
|
||||
{
|
||||
if (! $model = AssetModel::find($modelId)) {
|
||||
return redirect()->route('models.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||
}
|
||||
|
||||
$this->authorize('update', $model);
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
if (! Storage::exists('private_uploads/assetmodels')) {
|
||||
Storage::makeDirectory('private_uploads/assetmodels', 775);
|
||||
}
|
||||
|
||||
foreach ($request->file('file') as $file) {
|
||||
|
||||
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$model->id,$file);
|
||||
|
||||
$model->logUpload($file_name, $request->get('notes'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withFragment('files')->with('success', trans('general.file_upload_success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withFragment('files')->with('error', trans('admin/hardware/message.upload.nofiles'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for permissions and display the file.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $modelId
|
||||
* @param int $fileId
|
||||
* @since [v1.0]
|
||||
*/
|
||||
public function show(AssetModel $model, $fileId = null) : StreamedResponse | Response | RedirectResponse | BinaryFileResponse
|
||||
{
|
||||
|
||||
$this->authorize('view', $model);
|
||||
|
||||
if (! $log = Actionlog::find($fileId)) {
|
||||
return response('No matching record for that model/file', 500)
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
$file = 'private_uploads/assetmodels/'.$log->filename;
|
||||
|
||||
if (! Storage::exists($file)) {
|
||||
return response('File '.$file.' not found on server', 404)
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
if (request('inline') == 'true') {
|
||||
|
||||
$headers = [
|
||||
'Content-Disposition' => 'inline',
|
||||
];
|
||||
|
||||
return Storage::download($file, $log->filename, $headers);
|
||||
}
|
||||
|
||||
return StorageHelper::downloader($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the associated file
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $modelId
|
||||
* @param int $fileId
|
||||
* @since [v1.0]
|
||||
*/
|
||||
public function destroy(AssetModel $model, $fileId = null) : RedirectResponse
|
||||
{
|
||||
$rel_path = 'private_uploads/assetmodels';
|
||||
$this->authorize('update', $model);
|
||||
$log = Actionlog::find($fileId);
|
||||
if ($log) {
|
||||
if (Storage::exists($rel_path.'/'.$log->filename)) {
|
||||
Storage::delete($rel_path.'/'.$log->filename);
|
||||
}
|
||||
$log->delete();
|
||||
|
||||
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
|
||||
|
||||
}
|
||||
}
|
||||
@@ -153,7 +153,8 @@ class AssetCheckinController extends Controller
|
||||
if ($asset->save()) {
|
||||
|
||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues));
|
||||
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))->with('success', trans('admin/hardware/message.checkin.success'));
|
||||
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
||||
->with('success', trans('admin/hardware/message.checkin.success'));
|
||||
}
|
||||
// Redirect to the asset management page with error
|
||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.error').$asset->getErrors());
|
||||
|
||||
@@ -65,6 +65,8 @@ class AssetCheckoutController extends Controller
|
||||
*/
|
||||
public function store(AssetCheckoutRequest $request, $assetId) : RedirectResponse
|
||||
{
|
||||
|
||||
|
||||
try {
|
||||
// Check if the asset exists
|
||||
if (! $asset = Asset::find($assetId)) {
|
||||
@@ -81,6 +83,7 @@ class AssetCheckoutController extends Controller
|
||||
$admin = auth()->user();
|
||||
|
||||
$target = $this->determineCheckoutTarget();
|
||||
session()->put(['checkout_to_type' => $target]);
|
||||
|
||||
$asset = $this->updateAssetLocation($asset, $target);
|
||||
|
||||
@@ -123,7 +126,7 @@ class AssetCheckoutController extends Controller
|
||||
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
|
||||
|
||||
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->get('note'), $request->get('name'))) {
|
||||
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
|
||||
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
||||
->with('success', trans('admin/hardware/message.checkout.success'));
|
||||
}
|
||||
// Redirect to the asset management page with error
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Assets;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use \Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
use \Illuminate\Http\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class AssetFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Upload a file to the server.
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
* @param int $assetId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*@since [v1.0]
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
*/
|
||||
public function store(UploadFileRequest $request, Asset $asset) : RedirectResponse
|
||||
{
|
||||
|
||||
$this->authorize('update', $asset);
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
if (! Storage::exists('private_uploads/assets')) {
|
||||
Storage::makeDirectory('private_uploads/assets', 775);
|
||||
}
|
||||
|
||||
foreach ($request->file('file') as $file) {
|
||||
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
|
||||
|
||||
$asset->logUpload($file_name, $request->get('notes'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.upload.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for permissions and display the file.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $assetId
|
||||
* @param int $fileId
|
||||
* @since [v1.0]
|
||||
*/
|
||||
public function show(Asset $asset, $fileId = null) : View | RedirectResponse | Response | StreamedResponse | BinaryFileResponse
|
||||
{
|
||||
|
||||
$this->authorize('view', $asset);
|
||||
|
||||
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
|
||||
$file = 'private_uploads/assets/'.$log->filename;
|
||||
|
||||
if ($log->action_type == 'audit') {
|
||||
$file = 'private_uploads/audits/'.$log->filename;
|
||||
}
|
||||
|
||||
try {
|
||||
return StorageHelper::showOrDownloadFile($file, $log->filename);
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->route('hardware.show', $asset)->with('error', trans('general.file_not_found'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return redirect()->route('hardware.show', $asset)->with('error', trans('general.log_record_not_found'));
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the associated file
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $assetId
|
||||
* @param int $fileId
|
||||
* @since [v1.0]
|
||||
*/
|
||||
public function destroy(Asset $asset, $fileId = null) : RedirectResponse
|
||||
{
|
||||
$this->authorize('update', $asset);
|
||||
$rel_path = 'private_uploads/assets';
|
||||
|
||||
if ($log = Actionlog::find($fileId)) {
|
||||
if (Storage::exists($rel_path.'/'.$log->filename)) {
|
||||
Storage::delete($rel_path.'/'.$log->filename);
|
||||
}
|
||||
$log->delete();
|
||||
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
|
||||
}
|
||||
|
||||
return redirect()->route('hardware.show', $asset)->with('error', trans('general.log_record_not_found'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -110,17 +110,35 @@ class AssetsController extends Controller
|
||||
// This is only necessary on create, not update, since bulk editing is handled
|
||||
// differently
|
||||
$asset_tags = $request->input('asset_tags');
|
||||
$model = AssetModel::find($request->input('model_id'));
|
||||
$serial_errors = [];
|
||||
$serials = $request->input('serials');
|
||||
|
||||
$settings = Setting::getSettings();
|
||||
|
||||
//Validate required serial based on model setting
|
||||
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
|
||||
if ($model && $model->require_serial === 1 && empty($serials[$a])) {
|
||||
$serial_errors["serials.$a"] = trans('admin/hardware/form.serial_required', ['number' => $a]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!empty($serial_errors)) {
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->withErrors($serial_errors);
|
||||
}
|
||||
|
||||
$asset = null;
|
||||
$companyId = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
$successes = [];
|
||||
$failures = [];
|
||||
$serials = $request->input('serials');
|
||||
$asset = null;
|
||||
|
||||
for ($a = 1; $a <= count($asset_tags); $a++) {
|
||||
for ($a = 1, $aMax = count($asset_tags); $a <= $aMax; $a++) {
|
||||
$asset = new Asset();
|
||||
$asset->model()->associate(AssetModel::find($request->input('model_id')));
|
||||
|
||||
$asset->model()->associate($model);
|
||||
$asset->name = $request->input('name');
|
||||
|
||||
// Check for a corresponding serial
|
||||
@@ -132,7 +150,7 @@ class AssetsController extends Controller
|
||||
$asset->asset_tag = $asset_tags[$a];
|
||||
}
|
||||
|
||||
$asset->company_id = Company::getIdForCurrentUser($request->input('company_id'));
|
||||
$asset->company_id = $companyId;
|
||||
$asset->model_id = $request->input('model_id');
|
||||
$asset->order_number = $request->input('order_number');
|
||||
$asset->notes = $request->input('notes');
|
||||
@@ -157,14 +175,21 @@ class AssetsController extends Controller
|
||||
$asset->location_id = $request->input('rtd_location_id', null);
|
||||
}
|
||||
|
||||
// Create the image (if one was chosen.)
|
||||
if ($request->has('image')) {
|
||||
if ($request->has('use_cloned_image')) {
|
||||
$cloned_model_img = Asset::select('image')->find($request->input('clone_image_from_id'));
|
||||
if ($cloned_model_img) {
|
||||
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
|
||||
$new_image = 'assets/'.$new_image_name;
|
||||
Storage::disk('public')->copy('assets/'.$cloned_model_img->image, $new_image);
|
||||
$asset->image = $new_image_name;
|
||||
}
|
||||
|
||||
} else {
|
||||
$asset = $request->handleImages($asset);
|
||||
}
|
||||
|
||||
// Update custom fields in the database.
|
||||
// Validation for these fields is handled through the AssetRequest form request
|
||||
$model = AssetModel::find($request->get('model_id'));
|
||||
|
||||
if (($model) && ($model->fieldset)) {
|
||||
foreach ($model->fieldset->fields as $field) {
|
||||
@@ -226,25 +251,32 @@ class AssetsController extends Controller
|
||||
$failures[] = join(",", $asset->getErrors()->all());
|
||||
}
|
||||
}
|
||||
if($request->get('redirect_option') === 'back'){
|
||||
session()->put(['redirect_option' => 'index']);
|
||||
} else {
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
}
|
||||
|
||||
session()->put(['checkout_to_type' => $request->get('checkout_to_type'),
|
||||
'other_redirect' => 'model' ]);
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
|
||||
|
||||
|
||||
if ($successes) {
|
||||
if ($failures) {
|
||||
//some succeeded, some failed
|
||||
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets')) //FIXME - not tested
|
||||
return Helper::getRedirectOption($request, $asset->id, 'Assets') //FIXME - not tested
|
||||
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]))
|
||||
->with('warning', trans_choice('admin/hardware/message.create.partial_failure', $failures, ['failures' => join("; ", $failures)]));
|
||||
} else {
|
||||
if (count($successes) == 1) {
|
||||
//the most common case, keeping it so we don't have to make every use of that translation string be trans_choice'ed
|
||||
//and re-translated
|
||||
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
|
||||
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
||||
->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset), 'id', 'tag' => e($asset->asset_tag)]));
|
||||
} else {
|
||||
//multi-success
|
||||
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
|
||||
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
||||
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]));
|
||||
}
|
||||
}
|
||||
@@ -265,6 +297,7 @@ class AssetsController extends Controller
|
||||
public function edit(Asset $asset) : View | RedirectResponse
|
||||
{
|
||||
$this->authorize($asset);
|
||||
session()->put('back_url', url()->previous());
|
||||
return view('hardware/edit')
|
||||
->with('item', $asset)
|
||||
->with('statuslabel_list', Helper::statusLabelList())
|
||||
@@ -330,7 +363,7 @@ class AssetsController extends Controller
|
||||
$asset->purchase_cost = $request->input('purchase_cost', null);
|
||||
$asset->purchase_date = $request->input('purchase_date', null);
|
||||
$asset->next_audit_date = $request->input('next_audit_date', null);
|
||||
if ($request->filled('purchase_date') && !$request->filled('asset_eol_date') && ($asset->model->eol > 0)) {
|
||||
if ($request->filled('purchase_date') && !$request->filled('asset_eol_date') && ($asset->model?->eol > 0)) {
|
||||
$asset->purchase_date = $request->input('purchase_date', null);
|
||||
$asset->asset_eol_date = Carbon::parse($request->input('purchase_date'))->addMonths($asset->model->eol)->format('Y-m-d');
|
||||
$asset->eol_explicit = false;
|
||||
@@ -346,7 +379,7 @@ class AssetsController extends Controller
|
||||
} else {
|
||||
$asset->eol_explicit = true;
|
||||
}
|
||||
} elseif (!$request->filled('asset_eol_date') && (($asset->model->eol) == 0)) {
|
||||
} elseif (!$request->filled('asset_eol_date') && (($asset->model?->eol) == 0)) {
|
||||
$asset->asset_eol_date = null;
|
||||
$asset->eol_explicit = false;
|
||||
}
|
||||
@@ -365,6 +398,7 @@ class AssetsController extends Controller
|
||||
$asset->assigned_to = null;
|
||||
$asset->assigned_type = null;
|
||||
$asset->accepted = null;
|
||||
$asset->last_checkin = now();
|
||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on asset update with '.$status->getStatuslabelType().' status', date('Y-m-d H:i:s'), $originalValues));
|
||||
}
|
||||
|
||||
@@ -408,6 +442,9 @@ class AssetsController extends Controller
|
||||
$model = AssetModel::find($request->get('model_id'));
|
||||
if (($model) && ($model->fieldset)) {
|
||||
foreach ($model->fieldset->fields as $field) {
|
||||
if ($field->element == 'checkbox' && !$request->has($field->db_column)) {
|
||||
$asset->{$field->db_column} = null;
|
||||
}
|
||||
if ($request->has($field->db_column)) {
|
||||
if ($field->field_encrypted == '1') {
|
||||
if (Gate::allows('assets.view.encrypted_custom_fields')) {
|
||||
@@ -427,11 +464,22 @@ class AssetsController extends Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
session()->put([
|
||||
'redirect_option' => $request->get('redirect_option'),
|
||||
'checkout_to_type' => $request->get('checkout_to_type'),
|
||||
'other_redirect' => $request->get('redirect_option') === 'other_redirect' ? 'model' : null,
|
||||
]);
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
|
||||
|
||||
if ($asset->save()) {
|
||||
//Validate required serial based on model setting
|
||||
if ($model && $model->require_serial === 1 && empty($serial[1])) {
|
||||
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
|
||||
->with('warning', trans('admin/hardware/form.serial_required_post_model_update', [
|
||||
'asset_model' => $model->name
|
||||
]));
|
||||
}
|
||||
if ($asset->save()) {
|
||||
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
||||
->with('success', trans('admin/hardware/message.update.success'));
|
||||
}
|
||||
|
||||
@@ -636,8 +684,9 @@ class AssetsController extends Controller
|
||||
*/
|
||||
public function getClone(Asset $asset)
|
||||
{
|
||||
$this->authorize('create', $asset);
|
||||
$this->authorize('create', Asset::class);
|
||||
$cloned = clone $asset;
|
||||
$cloned_model = $asset;
|
||||
$cloned->id = null;
|
||||
$cloned->asset_tag = '';
|
||||
$cloned->serial = '';
|
||||
@@ -647,6 +696,7 @@ class AssetsController extends Controller
|
||||
return view('hardware/edit')
|
||||
->with('statuslabel_list', Helper::statusLabelList())
|
||||
->with('statuslabel_types', Helper::statusTypeList())
|
||||
->with('cloned_model', $cloned_model)
|
||||
->with('item', $cloned);
|
||||
}
|
||||
|
||||
@@ -772,7 +822,7 @@ class AssetsController extends Controller
|
||||
'item_id' => $asset->id,
|
||||
'item_type' => Asset::class,
|
||||
'created_by' => auth()->id(),
|
||||
'note' => 'Checkout imported by '.auth()->user()->present()->fullName().' from history importer',
|
||||
'note' => 'Checkout imported by '.auth()->user()->display_name.' from history importer',
|
||||
'target_id' => $item[$asset_tag][$batch_counter]['user_id'],
|
||||
'target_type' => User::class,
|
||||
'created_at' => $item[$asset_tag][$batch_counter]['checkout_date'],
|
||||
@@ -800,7 +850,7 @@ class AssetsController extends Controller
|
||||
'item_id' => $item[$asset_tag][$batch_counter]['asset_id'],
|
||||
'item_type' => Asset::class,
|
||||
'created_by' => auth()->id(),
|
||||
'note' => 'Checkin imported by '.auth()->user()->present()->fullName().' from history importer',
|
||||
'note' => 'Checkin imported by '.auth()->user()->display_name.' from history importer',
|
||||
'target_id' => null,
|
||||
'created_at' => $checkin_date,
|
||||
'action_type' => 'checkin',
|
||||
@@ -996,7 +1046,7 @@ class AssetsController extends Controller
|
||||
}
|
||||
|
||||
$asset->logAudit($request->input('note'), $request->input('location_id'), $file_name, $originalValues);
|
||||
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))->with('success', trans('admin/hardware/message.audit.success'));
|
||||
return Helper::getRedirectOption($request, $asset->id, 'Assets')->with('success', trans('admin/hardware/message.audit.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->withErrors($asset->getErrors());
|
||||
|
||||
@@ -112,11 +112,47 @@ class BulkAssetsController extends Controller
|
||||
// This handles all of the pivot sorting below (versus the assets.* fields in the allowed_columns array)
|
||||
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.id';
|
||||
|
||||
$assets = Asset::with('assignedTo', 'location', 'model')
|
||||
$query = Asset::with('assignedTo', 'location', 'model')
|
||||
->whereIn('assets.id', $asset_ids)
|
||||
->withTrashed();
|
||||
|
||||
$assets = $assets->get();
|
||||
|
||||
switch ($sort_override) {
|
||||
case 'model':
|
||||
$query->OrderModels($order);
|
||||
break;
|
||||
case 'model_number':
|
||||
$query->OrderModelNumber($order);
|
||||
break;
|
||||
case 'category':
|
||||
$query->OrderCategory($order);
|
||||
break;
|
||||
case 'manufacturer':
|
||||
$query->OrderManufacturer($order);
|
||||
break;
|
||||
case 'company':
|
||||
$query->OrderCompany($order);
|
||||
break;
|
||||
case 'location':
|
||||
$query->OrderLocation($order);
|
||||
break;
|
||||
case 'rtd_location':
|
||||
$query->OrderRtdLocation($order);
|
||||
break;
|
||||
case 'status_label':
|
||||
$query->OrderStatus($order);
|
||||
break;
|
||||
case 'supplier':
|
||||
$query->OrderSupplier($order);
|
||||
break;
|
||||
case 'assigned_to':
|
||||
$query->OrderAssigned($order);
|
||||
break;
|
||||
default:
|
||||
$query->orderBy($column_sort, $order);
|
||||
break;
|
||||
}
|
||||
$assets = $query->get();
|
||||
|
||||
if ($assets->isEmpty()) {
|
||||
Log::debug('No assets were found for the provided IDs', ['ids' => $asset_ids]);
|
||||
@@ -125,8 +161,9 @@ class BulkAssetsController extends Controller
|
||||
|
||||
$models = $assets->unique('model_id');
|
||||
$modelNames = [];
|
||||
|
||||
foreach($models as $model) {
|
||||
$modelNames[] = $model->model->name;
|
||||
$modelNames[] = $model->model?->name;
|
||||
}
|
||||
|
||||
if ($request->filled('bulk_actions')) {
|
||||
@@ -160,7 +197,6 @@ class BulkAssetsController extends Controller
|
||||
|
||||
case 'edit':
|
||||
$this->authorize('update', Asset::class);
|
||||
|
||||
return view('hardware/bulk')
|
||||
->with('assets', $asset_ids)
|
||||
->with('statuslabel_list', Helper::statusLabelList())
|
||||
@@ -169,40 +205,7 @@ class BulkAssetsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
switch ($sort_override) {
|
||||
case 'model':
|
||||
$assets->OrderModels($order);
|
||||
break;
|
||||
case 'model_number':
|
||||
$assets->OrderModelNumber($order);
|
||||
break;
|
||||
case 'category':
|
||||
$assets->OrderCategory($order);
|
||||
break;
|
||||
case 'manufacturer':
|
||||
$assets->OrderManufacturer($order);
|
||||
break;
|
||||
case 'company':
|
||||
$assets->OrderCompany($order);
|
||||
break;
|
||||
case 'location':
|
||||
$assets->OrderLocation($order);
|
||||
case 'rtd_location':
|
||||
$assets->OrderRtdLocation($order);
|
||||
break;
|
||||
case 'status_label':
|
||||
$assets->OrderStatus($order);
|
||||
break;
|
||||
case 'supplier':
|
||||
$assets->OrderSupplier($order);
|
||||
break;
|
||||
case 'assigned_to':
|
||||
$assets->OrderAssigned($order);
|
||||
break;
|
||||
default:
|
||||
$assets->orderBy($column_sort, $order);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return redirect()->back()->with('error', 'No action selected');
|
||||
}
|
||||
@@ -221,11 +224,8 @@ class BulkAssetsController extends Controller
|
||||
$error_array = array();
|
||||
|
||||
// Get the back url from the session and then destroy the session
|
||||
$bulk_back_url = route('hardware.index');
|
||||
|
||||
if ($request->session()->has('bulk_back_url')) {
|
||||
$bulk_back_url = $request->session()->pull('bulk_back_url');
|
||||
}
|
||||
$bulk_back_url = $request->session()->pull('bulk_back_url', url()->previous());
|
||||
|
||||
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
|
||||
|
||||
@@ -241,10 +241,6 @@ class BulkAssetsController extends Controller
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (! $request->filled('ids') || count($request->input('ids')) == 0) {
|
||||
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
|
||||
}
|
||||
@@ -274,6 +270,7 @@ class BulkAssetsController extends Controller
|
||||
|| ($request->filled('company_id'))
|
||||
|| ($request->filled('status_id'))
|
||||
|| ($request->filled('model_id'))
|
||||
|| ($request->filled('notes'))
|
||||
|| ($request->filled('next_audit_date'))
|
||||
|| ($request->filled('asset_eol_date'))
|
||||
|| ($request->filled('null_name'))
|
||||
@@ -281,6 +278,7 @@ class BulkAssetsController extends Controller
|
||||
|| ($request->filled('null_expected_checkin_date'))
|
||||
|| ($request->filled('null_next_audit_date'))
|
||||
|| ($request->filled('null_asset_eol_date'))
|
||||
|| ($request->filled('null_notes'))
|
||||
|| ($request->anyFilled($custom_field_columns))
|
||||
|| ($request->anyFilled(array_keys($null_custom_fields_inputs)))
|
||||
|
||||
@@ -305,7 +303,8 @@ class BulkAssetsController extends Controller
|
||||
->conditionallyAddItem('supplier_id')
|
||||
->conditionallyAddItem('warranty_months')
|
||||
->conditionallyAddItem('next_audit_date')
|
||||
->conditionallyAddItem('asset_eol_date');
|
||||
->conditionallyAddItem('asset_eol_date')
|
||||
->conditionallyAddItem('notes');
|
||||
foreach ($custom_field_columns as $key => $custom_field_column) {
|
||||
$this->conditionallyAddItem($custom_field_column);
|
||||
}
|
||||
@@ -362,6 +361,10 @@ class BulkAssetsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->input('null_notes')=='1') {
|
||||
$this->update_array['notes'] = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ($request->filled('purchase_cost')) {
|
||||
@@ -464,7 +467,7 @@ class BulkAssetsController extends Controller
|
||||
*/
|
||||
|
||||
// Does the model have a fieldset?
|
||||
if ($asset->model->fieldset) {
|
||||
if ($asset->model?->fieldset) {
|
||||
foreach ($asset->model->fieldset->fields as $field) {
|
||||
|
||||
// null custom fields
|
||||
@@ -534,7 +537,13 @@ class BulkAssetsController extends Controller
|
||||
} // end asset foreach
|
||||
|
||||
if ($has_errors > 0) {
|
||||
return redirect($bulk_back_url)->with('bulk_asset_errors', $error_array);
|
||||
session()->put('bulkedit_ids', $request->input('ids'));
|
||||
session()->put('bulk_asset_errors',$error_array);
|
||||
|
||||
return redirect()
|
||||
->route('hardware.index')
|
||||
->with('bulk_asset_errors', $error_array)
|
||||
->withInput();
|
||||
}
|
||||
|
||||
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success'));
|
||||
@@ -609,9 +618,25 @@ class BulkAssetsController extends Controller
|
||||
{
|
||||
$this->authorize('checkout', Asset::class);
|
||||
|
||||
$alreadyAssigned = collect();
|
||||
|
||||
if (old('selected_assets') && is_array(old('selected_assets'))) {
|
||||
$assets = Asset::findMany(old('selected_assets'));
|
||||
|
||||
[$assignable, $alreadyAssigned] = $assets->partition(function (Asset $asset) {
|
||||
return !$asset->assigned_to;
|
||||
});
|
||||
|
||||
session()->flashInput(['selected_assets' => $assignable->pluck('id')->values()->toArray()]);
|
||||
}
|
||||
|
||||
$do_not_change = ['' => trans('general.do_not_change')];
|
||||
$status_label_list = $do_not_change + Helper::deployableStatusLabelList();
|
||||
return view('hardware/bulk-checkout')->with('statusLabel_list', $status_label_list);
|
||||
|
||||
return view('hardware/bulk-checkout', [
|
||||
'statusLabel_list' => $status_label_list,
|
||||
'removed_assets' => $alreadyAssigned,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -625,6 +650,7 @@ class BulkAssetsController extends Controller
|
||||
$admin = auth()->user();
|
||||
|
||||
$target = $this->determineCheckoutTarget();
|
||||
session()->put(['checkout_to_type' => $target]);
|
||||
|
||||
if (! is_array($request->get('selected_assets'))) {
|
||||
return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans('admin/hardware/message.checkout.no_assets_selected'));
|
||||
@@ -634,6 +660,30 @@ class BulkAssetsController extends Controller
|
||||
|
||||
$assets = Asset::findOrFail($asset_ids);
|
||||
|
||||
// Prevent checking out assets that are already checked out
|
||||
if ($assets->pluck('assigned_to')->unique()->filter()->isNotEmpty()) {
|
||||
// re-add the asset ids so the assets select is re-populated
|
||||
$request->session()->flashInput(['selected_assets' => $asset_ids]);
|
||||
|
||||
return redirect(route('hardware.bulkcheckout.show'))
|
||||
->with('error', trans('general.error_assets_already_checked_out'));
|
||||
}
|
||||
|
||||
// Prevent checking out assets across companies if FMCS enabled
|
||||
if (Setting::getSettings()->full_multiple_companies_support && $target->company_id) {
|
||||
$company_ids = $assets->pluck('company_id')->unique();
|
||||
|
||||
// if there is more than one unique company id or the singular company id does not match
|
||||
// then the checkout is invalid
|
||||
if ($company_ids->count() > 1 || $company_ids->first() != $target->company_id) {
|
||||
// re-add the asset ids so the assets select is re-populated
|
||||
$request->session()->flashInput(['selected_assets' => $asset_ids]);
|
||||
|
||||
return redirect(route('hardware.bulkcheckout.show'))
|
||||
->with('error', trans('general.error_user_company_multiple'));
|
||||
}
|
||||
}
|
||||
|
||||
if (request('checkout_to_type') == 'asset') {
|
||||
foreach ($asset_ids as $asset_id) {
|
||||
if ($target->id == $asset_id) {
|
||||
@@ -726,4 +776,33 @@ class BulkAssetsController extends Controller
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function bulkEditForm(): View|RedirectResponse
|
||||
{
|
||||
$this->authorize('update', Asset::class);
|
||||
|
||||
$asset_ids = session()->pull('bulkedit_ids', []);
|
||||
|
||||
if (empty($asset_ids)) {
|
||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.update.no_assets_selected'));
|
||||
}
|
||||
|
||||
$assets = Asset::with('model')->withTrashed()->whereIn('id', $asset_ids)->get();
|
||||
|
||||
if ($assets->isEmpty()) {
|
||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.update.assets_do_not_exist_or_are_invalid'));
|
||||
}
|
||||
|
||||
$models = $assets->unique('model_id');
|
||||
$modelNames = [];
|
||||
foreach ($models as $model) {
|
||||
$modelNames[] = $model->model->name;
|
||||
}
|
||||
|
||||
return view('hardware/bulk')
|
||||
->with('assets', $asset_ids)
|
||||
->with('statuslabel_list', Helper::statusLabelList())
|
||||
->with('models', $models->pluck(['model']))
|
||||
->with('modelNames', $modelNames);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,9 @@ class BulkAssetModelsController extends Controller
|
||||
$update_array['min_amt'] = $request->input('min_amt');
|
||||
}
|
||||
|
||||
|
||||
if ($request->filled('require_serial')) {
|
||||
$update_array['require_serial'] = $request->input('require_serial');
|
||||
}
|
||||
|
||||
if (count($update_array) > 0) {
|
||||
AssetModel::whereIn('id', $models_raw_array)->update($update_array);
|
||||
|
||||
59
app/Http/Controllers/BulkCategoriesController.php
Normal file
59
app/Http/Controllers/BulkCategoriesController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Categories\DestroyCategoryAction;
|
||||
use App\Exceptions\ItemStillHasAccessories;
|
||||
use App\Exceptions\ItemStillHasAssetModels;
|
||||
use App\Exceptions\ItemStillHasAssets;
|
||||
use App\Exceptions\ItemStillHasComponents;
|
||||
use App\Exceptions\ItemStillHasConsumables;
|
||||
use App\Exceptions\ItemStillHasLicenses;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BulkCategoriesController extends Controller
|
||||
{
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$this->authorize('delete', Category::class);
|
||||
|
||||
$errors = [];
|
||||
$success_count = 0;
|
||||
|
||||
foreach ($request->ids as $id) {
|
||||
$category = Category::find($id);
|
||||
if (is_null($category)) {
|
||||
$errors[] = trans('admin/categories/message.does_not_exist');
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
DestroyCategoryAction::run(category: $category);
|
||||
$success_count++;
|
||||
} catch (ItemStillHasAccessories $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_assets_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
|
||||
} catch (ItemStillHasAssetModels) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_asset_models_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
|
||||
} catch (ItemStillHasAssets) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_assets_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
|
||||
} catch (ItemStillHasComponents) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_components_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
|
||||
} catch (ItemStillHasConsumables) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_consumables_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);
|
||||
} catch (ItemStillHasLicenses) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_licenses_no_count', ['item_name' => $category->name, 'item' => trans('general.category')]);;
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
$errors[] = trans('general.something_went_wrong');
|
||||
}
|
||||
}
|
||||
if (count($errors) > 0) {
|
||||
if ($success_count > 0) {
|
||||
return redirect()->route('categories.index')->with('success', trans_choice('admin/categories/message.delete.partial_success', $success_count, ['count' => $success_count]))->with('multi_error_messages', $errors);
|
||||
}
|
||||
return redirect()->route('categories.index')->with('multi_error_messages', $errors);
|
||||
} else {
|
||||
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.bulk_success'));
|
||||
}
|
||||
}
|
||||
}
|
||||
57
app/Http/Controllers/BulkManufacturersController.php
Normal file
57
app/Http/Controllers/BulkManufacturersController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Manufacturers\DeleteManufacturerAction;
|
||||
use App\Exceptions\ItemStillHasAccessories;
|
||||
use App\Exceptions\ItemStillHasAssetModels;
|
||||
use App\Exceptions\ItemStillHasAssets;
|
||||
use App\Exceptions\ItemStillHasChildren;
|
||||
use App\Exceptions\ItemStillHasComponents;
|
||||
use App\Exceptions\ItemStillHasConsumables;
|
||||
use App\Exceptions\ItemStillHasLicenses;
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BulkManufacturersController extends Controller
|
||||
{
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$this->authorize('delete', Manufacturer::class);
|
||||
|
||||
$errors = [];
|
||||
$success_count = 0;
|
||||
foreach ($request->ids as $id) {
|
||||
$manufacturer = Manufacturer::find($id);
|
||||
if (is_null($manufacturer)) {
|
||||
$errors[] = trans('admin/manufacturers/message.does_not_exist');
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
DeleteManufacturerAction::run(manufacturer: $manufacturer);
|
||||
$success_count++;
|
||||
} catch (ItemStillHasAssets $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_assets_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
|
||||
} catch (ItemStillHasAccessories $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_accessories_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
|
||||
} catch (ItemStillHasConsumables $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_consumables_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
|
||||
} catch (ItemStillHasComponents $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_components_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);
|
||||
} catch (ItemStillHasLicenses $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_licenses_no_count', ['item_name' => $manufacturer->name, 'item' => trans('general.manufacturer')]);;
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
$errors[] = trans('general.something_went_wrong');
|
||||
}
|
||||
}
|
||||
if (count($errors) > 0) {
|
||||
if ($success_count > 0) {
|
||||
return redirect()->route('manufacturers.index')->with('success', trans_choice('admin/manufacturers/message.delete.partial_success', $success_count, ['count' => $success_count]))->with('multi_error_messages', $errors);
|
||||
}
|
||||
return redirect()->route('manufacturers.index')->with('multi_error_messages', $errors);
|
||||
} else {
|
||||
return redirect()->route('manufacturers.index')->with('success', trans('admin/manufacturers/message.delete.bulk_success'));
|
||||
}
|
||||
}
|
||||
}
|
||||
58
app/Http/Controllers/BulkSuppliersController.php
Normal file
58
app/Http/Controllers/BulkSuppliersController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Suppliers\DestroySupplierAction;
|
||||
use App\Exceptions\ItemStillHasAccessories;
|
||||
use App\Exceptions\ItemStillHasComponents;
|
||||
use App\Exceptions\ItemStillHasConsumables;
|
||||
use App\Exceptions\ItemStillHasMaintenances;
|
||||
use App\Exceptions\ItemStillHasAssets;
|
||||
use App\Exceptions\ItemStillHasLicenses;
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BulkSuppliersController extends Controller
|
||||
{
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$this->authorize('delete', Supplier::class);
|
||||
|
||||
$errors = [];
|
||||
$success_count = 0;
|
||||
|
||||
foreach ($request->ids as $id) {
|
||||
$supplier = Supplier::find($id);
|
||||
if (is_null($supplier)) {
|
||||
$errors[] = trans('admin/suppliers/message.delete.not_found');
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
DestroySupplierAction::run(supplier: $supplier);
|
||||
} catch (ItemStillHasAssets $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_assets', ['asset_count' => (int) $supplier->assets_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
|
||||
} catch (ItemStillHasMaintenances $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
|
||||
} catch (ItemStillHasLicenses $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_licenses', ['licenses_count' => (int) $supplier->licenses_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
|
||||
} catch (ItemStillHasAccessories $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_accessories', ['accessories_count' => (int) $supplier->accessories_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
|
||||
} catch (ItemStillHasConsumables $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_consumables', ['consumables_count' => (int) $supplier->consumables_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
|
||||
} catch (ItemStillHasComponents $e) {
|
||||
$errors[] = trans('general.bulk_delete_associations.assoc_components', ['components_count' => (int) $supplier->components_count, 'item' => trans('general.supplier'), 'item_name' => $supplier->name]);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
$errors[] = trans('general.something_went_wrong');
|
||||
}
|
||||
}
|
||||
if (count($errors) > 0) {
|
||||
if ($success_count > 0) {
|
||||
return redirect()->route('suppliers.index')->with('success', trans_choice('admin/suppliers/message.delete.partial_success', $success_count, ['count' => $success_count]))->with('multi_error_messages', $errors);
|
||||
}
|
||||
return redirect()->route('suppliers.index')->with('multi_error_messages', $errors);
|
||||
} else {
|
||||
return redirect()->route('suppliers.index')->with('success', trans('admin/suppliers/message.delete.bulk_success'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Categories\DestroyCategoryAction;
|
||||
use App\Exceptions\ItemStillHasAccessories;
|
||||
use App\Exceptions\ItemStillHasAssetModels;
|
||||
use App\Exceptions\ItemStillHasAssets;
|
||||
use App\Exceptions\ItemStillHasChildren;
|
||||
use App\Exceptions\ItemStillHasComponents;
|
||||
use App\Exceptions\ItemStillHasConsumables;
|
||||
use App\Exceptions\ItemStillHasLicenses;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use App\Models\Category;
|
||||
@@ -68,6 +76,7 @@ class CategoriesController extends Controller
|
||||
$category->eula_text = $request->input('eula_text');
|
||||
$category->use_default_eula = $request->input('use_default_eula', '0');
|
||||
$category->require_acceptance = $request->input('require_acceptance', '0');
|
||||
$category->alert_on_response = $request->input('alert_on_response', '0');
|
||||
$category->checkin_email = $request->input('checkin_email', '0');
|
||||
$category->notes = $request->input('notes');
|
||||
$category->created_by = auth()->id();
|
||||
@@ -121,6 +130,7 @@ class CategoriesController extends Controller
|
||||
$category->eula_text = $request->input('eula_text');
|
||||
$category->use_default_eula = $request->input('use_default_eula', '0');
|
||||
$category->require_acceptance = $request->input('require_acceptance', '0');
|
||||
$category->alert_on_response = $request->input('alert_on_response', '0');
|
||||
$category->checkin_email = $request->input('checkin_email', '0');
|
||||
$category->notes = $request->input('notes');
|
||||
|
||||
@@ -141,20 +151,18 @@ class CategoriesController extends Controller
|
||||
* @since [v1.0]
|
||||
* @param int $categoryId
|
||||
*/
|
||||
public function destroy($categoryId) : RedirectResponse
|
||||
public function destroy(Category $category): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', Category::class);
|
||||
// Check if the category exists
|
||||
if (is_null($category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($categoryId))) {
|
||||
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.not_found'));
|
||||
try {
|
||||
DestroyCategoryAction::run($category);
|
||||
} catch (ItemStillHasChildren $e) {
|
||||
return redirect()->route('categories.index')->with('error', trans('general.bulk_delete_associations.general_assoc_warning', ['item' => trans('general.category')]));
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.delete.error'));
|
||||
}
|
||||
|
||||
if (! $category->isDeletable()) {
|
||||
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.assoc_items', ['asset_type'=> $category->category_type]));
|
||||
}
|
||||
|
||||
Storage::disk('public')->delete('categories'.'/'.$category->image);
|
||||
$category->delete();
|
||||
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.success'));
|
||||
}
|
||||
|
||||
|
||||
@@ -123,11 +123,13 @@ final class CompaniesController extends Controller
|
||||
*/
|
||||
public function destroy($companyId) : RedirectResponse
|
||||
{
|
||||
|
||||
if (is_null($company = Company::find($companyId))) {
|
||||
return redirect()->route('companies.index')
|
||||
->with('error', trans('admin/companies/message.not_found'));
|
||||
}
|
||||
|
||||
|
||||
$this->authorize('delete', $company);
|
||||
if (! $company->isDeletable()) {
|
||||
return redirect()->route('companies.index')
|
||||
|
||||
@@ -100,8 +100,8 @@ class ComponentCheckinController extends Controller
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
|
||||
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success',
|
||||
trans('admin/components/message.checkin.success'));
|
||||
return Helper::getRedirectOption($request, $component->id, 'Components')
|
||||
->with('success', trans('admin/components/message.checkin.success'));
|
||||
}
|
||||
|
||||
return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));
|
||||
|
||||
@@ -102,13 +102,15 @@ class ComponentCheckoutController extends Controller
|
||||
return redirect()->route('components.checkout.show', $componentId)->with('error', trans('general.error_user_company'));
|
||||
}
|
||||
|
||||
$component->checkout_qty = $request->input('assigned_qty');
|
||||
|
||||
// Update the component data
|
||||
$component->asset_id = $request->input('asset_id');
|
||||
$component->assets()->attach($component->id, [
|
||||
'component_id' => $component->id,
|
||||
'created_by' => auth()->user()->id,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'assigned_qty' => $request->input('assigned_qty'),
|
||||
'assigned_qty' => $component->checkout_qty,
|
||||
'asset_id' => $request->input('asset_id'),
|
||||
'note' => $request->input('note'),
|
||||
]);
|
||||
@@ -120,6 +122,7 @@ class ComponentCheckoutController extends Controller
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
|
||||
|
||||
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.checkout.success'));
|
||||
return Helper::getRedirectOption($request, $component->id, 'Components')
|
||||
->with('success', trans('admin/components/message.checkout.success'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,10 +88,16 @@ class ComponentsController extends Controller
|
||||
|
||||
$component = $request->handleImages($component);
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
if($request->get('redirect_option') === 'back'){
|
||||
session()->put(['redirect_option' => 'index']);
|
||||
} else {
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
}
|
||||
|
||||
|
||||
if ($component->save()) {
|
||||
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.create.success'));
|
||||
return Helper::getRedirectOption($request, $component->id, 'Components')
|
||||
->with('success', trans('admin/components/message.create.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->withErrors($component->getErrors());
|
||||
@@ -111,6 +117,7 @@ class ComponentsController extends Controller
|
||||
{
|
||||
|
||||
$this->authorize('update', $component);
|
||||
session()->put('back_url', url()->previous());
|
||||
return view('components/edit')
|
||||
->with('item', $component)
|
||||
->with('category_type', 'component');
|
||||
@@ -164,7 +171,8 @@ class ComponentsController extends Controller
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
|
||||
if ($component->save()) {
|
||||
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.update.success'));
|
||||
return Helper::getRedirectOption($request, $component->id, 'Components')
|
||||
->with('success', trans('admin/components/message.update.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->withErrors($component->getErrors());
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Components;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Component;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ComponentsFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Validates and stores files associated with a component.
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
* @param int $componentId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*@author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @todo Switch to using the AssetFileRequest form request validator.
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $componentId = null)
|
||||
{
|
||||
|
||||
if (config('app.lock_passwords')) {
|
||||
return redirect()->route('components.show', ['component'=>$componentId])->with('error', trans('general.feature_disabled'));
|
||||
}
|
||||
|
||||
$component = Component::find($componentId);
|
||||
|
||||
if (isset($component->id)) {
|
||||
$this->authorize('update', $component);
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
if (! Storage::exists('private_uploads/components')) {
|
||||
Storage::makeDirectory('private_uploads/components', 775);
|
||||
}
|
||||
|
||||
foreach ($request->file('file') as $file) {
|
||||
$file_name = $request->handleFile('private_uploads/components/','component-'.$component->id, $file);
|
||||
|
||||
//Log the upload to the log
|
||||
$component->logUpload($file_name, e($request->input('notes')));
|
||||
}
|
||||
|
||||
|
||||
return redirect()->route('components.show', $component->id)->withFragment('files')->with('success', trans('general.file_upload_success'));
|
||||
|
||||
}
|
||||
|
||||
return redirect()->route('components.show', $component->id)->with('error', trans('general.no_files_uploaded'));
|
||||
}
|
||||
// Prepare the error message
|
||||
return redirect()->route('components.index')
|
||||
->with('error', trans('general.file_does_not_exist'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the selected component file.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @param int $componentId
|
||||
* @param int $fileId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function destroy($componentId = null, $fileId = null)
|
||||
{
|
||||
$component = Component::find($componentId);
|
||||
|
||||
// the asset is valid
|
||||
if (isset($component->id)) {
|
||||
$this->authorize('update', $component);
|
||||
$log = Actionlog::find($fileId);
|
||||
|
||||
// Remove the file if one exists
|
||||
if (Storage::exists('components/'.$log->filename)) {
|
||||
try {
|
||||
Storage::delete('components/'.$log->filename);
|
||||
} catch (\Exception $e) {
|
||||
Log::debug($e);
|
||||
}
|
||||
}
|
||||
|
||||
$log->delete();
|
||||
|
||||
return redirect()->back()->withFragment('files')
|
||||
->with('success', trans('admin/hardware/message.deletefile.success'));
|
||||
}
|
||||
|
||||
// Redirect to the licence management page
|
||||
return redirect()->route('components.index')->with('error', trans('general.file_does_not_exist'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the selected file to be viewed.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.4]
|
||||
* @param int $componentId
|
||||
* @param int $fileId
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function show($componentId = null, $fileId = null)
|
||||
{
|
||||
Log::debug('Private filesystem is: '.config('filesystems.default'));
|
||||
|
||||
|
||||
// the component is valid
|
||||
if ($component = Component::find($componentId)) {
|
||||
$this->authorize('view', $component);
|
||||
$this->authorize('components.files', $component);
|
||||
|
||||
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $component->id)->find($fileId)) {
|
||||
|
||||
$file = 'private_uploads/components/'.$log->filename;
|
||||
|
||||
try {
|
||||
return StorageHelper::showOrDownloadFile($file, $log->filename);
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->route('components.show', ['component' => $component])->with('error', trans('general.file_not_found'));
|
||||
}
|
||||
}
|
||||
return redirect()->route('components.show', ['component' => $component])->with('error', trans('general.log_record_not_found'));
|
||||
|
||||
}
|
||||
|
||||
return redirect()->route('components.index')->with('error', trans('general.file_does_not_exist', ['id' => $fileId]));
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,7 @@ class ConsumableCheckoutController extends Controller
|
||||
|
||||
|
||||
// Redirect to the new consumable page
|
||||
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.checkout.success'));
|
||||
return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
|
||||
->with('success', trans('admin/consumables/message.checkout.success'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use App\Models\Company;
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use \Illuminate\Contracts\View\View;
|
||||
@@ -81,16 +81,33 @@ class ConsumablesController extends Controller
|
||||
$consumable->purchase_date = $request->input('purchase_date');
|
||||
$consumable->purchase_cost = $request->input('purchase_cost');
|
||||
$consumable->qty = $request->input('qty');
|
||||
$consumable->created_by = auth()->id();
|
||||
$consumable->created_by = auth()->id();
|
||||
$consumable->notes = $request->input('notes');
|
||||
|
||||
|
||||
$consumable = $request->handleImages($consumable);
|
||||
if ($request->has('use_cloned_image')) {
|
||||
$cloned_model_img = Consumable::select('image')->find($request->input('clone_image_from_id'));
|
||||
if ($cloned_model_img) {
|
||||
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
|
||||
$new_image = 'consumables/'.$new_image_name;
|
||||
Storage::disk('public')->copy('consumables/'.$cloned_model_img->image, $new_image);
|
||||
$consumable->image = $new_image_name;
|
||||
}
|
||||
|
||||
} else {
|
||||
$consumable = $request->handleImages($consumable);
|
||||
}
|
||||
|
||||
if($request->get('redirect_option') === 'back'){
|
||||
session()->put(['redirect_option' => 'index']);
|
||||
} else {
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
}
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
|
||||
if ($consumable->save()) {
|
||||
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.create.success'));
|
||||
return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
|
||||
->with('success', trans('admin/consumables/message.create.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->withErrors($consumable->getErrors());
|
||||
@@ -107,6 +124,7 @@ class ConsumablesController extends Controller
|
||||
public function edit(Consumable $consumable) : View | RedirectResponse
|
||||
{
|
||||
$this->authorize($consumable);
|
||||
session()->put('back_url', url()->previous());
|
||||
return view('consumables/edit')
|
||||
->with('item', $consumable)
|
||||
->with('category_type', 'consumable');
|
||||
@@ -160,7 +178,8 @@ class ConsumablesController extends Controller
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
|
||||
if ($consumable->save()) {
|
||||
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.update.success'));
|
||||
return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
|
||||
->with('success', trans('admin/consumables/message.update.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->withErrors($consumable->getErrors());
|
||||
@@ -210,9 +229,10 @@ class ConsumablesController extends Controller
|
||||
$consumable_to_close = $consumable;
|
||||
$consumable = clone $consumable_to_close;
|
||||
$consumable->id = null;
|
||||
$consumable->image = null;
|
||||
$consumable->created_by = null;
|
||||
|
||||
return view('consumables/edit')->with('item', $consumable);
|
||||
return view('consumables/edit')
|
||||
->with('cloned_model', $consumable_to_close)
|
||||
->with('item', $consumable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Consumables;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Consumable;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Consumable\HttpFoundation\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
class ConsumablesFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Validates and stores files associated with a consumable.
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
* @param int $consumableId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*@author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @todo Switch to using the AssetFileRequest form request validator.
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $consumableId = null)
|
||||
{
|
||||
if (config('app.lock_passwords')) {
|
||||
return redirect()->route('consumables.show', ['consumable'=>$consumableId])->with('error', trans('general.feature_disabled'));
|
||||
}
|
||||
|
||||
$consumable = Consumable::find($consumableId);
|
||||
|
||||
if (isset($consumable->id)) {
|
||||
$this->authorize('update', $consumable);
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
if (! Storage::exists('private_uploads/consumables')) {
|
||||
Storage::makeDirectory('private_uploads/consumables', 775);
|
||||
}
|
||||
|
||||
foreach ($request->file('file') as $file) {
|
||||
$file_name = $request->handleFile('private_uploads/consumables/','consumable-'.$consumable->id, $file);
|
||||
|
||||
//Log the upload to the log
|
||||
$consumable->logUpload($file_name, e($request->input('notes')));
|
||||
}
|
||||
|
||||
|
||||
return redirect()->route('consumables.show', $consumable->id)->withFragment('files')->with('success', trans('general.file_upload_success'));
|
||||
|
||||
}
|
||||
|
||||
return redirect()->route('consumables.show', $consumable->id)->with('error', trans('general.no_files_uploaded'));
|
||||
}
|
||||
// Prepare the error message
|
||||
return redirect()->route('consumables.index')
|
||||
->with('error', trans('general.file_does_not_exist'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the selected consumable file.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @param int $consumableId
|
||||
* @param int $fileId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function destroy($consumableId = null, $fileId = null)
|
||||
{
|
||||
$consumable = Consumable::find($consumableId);
|
||||
|
||||
// the asset is valid
|
||||
if (isset($consumable->id)) {
|
||||
$this->authorize('update', $consumable);
|
||||
$log = Actionlog::find($fileId);
|
||||
|
||||
// Remove the file if one exists
|
||||
if (Storage::exists('consumables/'.$log->filename)) {
|
||||
try {
|
||||
Storage::delete('consumables/'.$log->filename);
|
||||
} catch (\Exception $e) {
|
||||
Log::debug($e);
|
||||
}
|
||||
}
|
||||
|
||||
$log->delete();
|
||||
|
||||
return redirect()->back()->withFragment('files')
|
||||
->with('success', trans('admin/hardware/message.deletefile.success'));
|
||||
}
|
||||
|
||||
// Redirect to the licence management page
|
||||
return redirect()->route('consumables.index')->with('error', trans('general.file_does_not_exist'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the selected file to be viewed.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.4]
|
||||
* @param int $consumableId
|
||||
* @param int $fileId
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function show($consumableId = null, $fileId = null)
|
||||
{
|
||||
$consumable = Consumable::find($consumableId);
|
||||
|
||||
// the consumable is valid
|
||||
if (isset($consumable->id)) {
|
||||
$this->authorize('view', $consumable);
|
||||
$this->authorize('consumables.files', $consumable);
|
||||
|
||||
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $consumable->id)->find($fileId)) {
|
||||
$file = 'private_uploads/consumables/'.$log->filename;
|
||||
|
||||
try {
|
||||
return StorageHelper::showOrDownloadFile($file, $log->filename);
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->route('consumables.show', ['consumable' => $consumable])->with('error', trans('general.file_not_found'));
|
||||
}
|
||||
}
|
||||
// The log record doesn't exist somehow
|
||||
return redirect()->route('consumables.show', ['consumable' => $consumable])->with('error', trans('general.log_record_not_found'));
|
||||
|
||||
}
|
||||
|
||||
return redirect()->route('consumables.index')->with('error', trans('general.file_does_not_exist', ['id' => $fileId]));
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,15 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\License;
|
||||
use App\Models\Location;
|
||||
use App\Models\Maintenance;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
@@ -32,6 +41,48 @@ abstract class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
|
||||
|
||||
static $map_object_type = [
|
||||
'accessories' => Accessory::class,
|
||||
'maintenances' => Maintenance::class,
|
||||
'assets' => Asset::class,
|
||||
'audits' => Asset::class,
|
||||
'components' => Component::class,
|
||||
'consumables' => Consumable::class,
|
||||
'hardware' => Asset::class,
|
||||
'licenses' => License::class,
|
||||
'locations' => Location::class,
|
||||
'models' => AssetModel::class,
|
||||
'users' => User::class,
|
||||
];
|
||||
|
||||
static $map_storage_path = [
|
||||
'accessories' => 'private_uploads/accessories/',
|
||||
'maintenances' => 'private_uploads/maintenances/',
|
||||
'assets' => 'private_uploads/assets/',
|
||||
'audits' => 'private_uploads/audits/',
|
||||
'components' => 'private_uploads/components/',
|
||||
'consumables' => 'private_uploads/consumables/',
|
||||
'hardware' => 'private_uploads/assets/',
|
||||
'licenses' => 'private_uploads/licenses/',
|
||||
'locations' => 'private_uploads/locations/',
|
||||
'models' => 'private_uploads/models/',
|
||||
'users' => 'private_uploads/users/',
|
||||
];
|
||||
|
||||
static $map_file_prefix= [
|
||||
'accessories' => 'accessory',
|
||||
'maintenances' => 'maintenance',
|
||||
'assets' => 'asset',
|
||||
'audits' => 'audits',
|
||||
'components' => 'component',
|
||||
'consumables' => 'consumable',
|
||||
'hardware' => 'asset',
|
||||
'licenses' => 'license',
|
||||
'locations' => 'location',
|
||||
'models' => 'model',
|
||||
'users' => 'user',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
view()->share('signedIn', Auth::check());
|
||||
|
||||
@@ -144,10 +144,9 @@ class CustomFieldsController extends Controller
|
||||
*/
|
||||
public function deleteFieldFromFieldset($field_id, $fieldset_id) : RedirectResponse
|
||||
{
|
||||
$this->authorize('update', CustomField::class);
|
||||
$field = CustomField::find($field_id);
|
||||
|
||||
$this->authorize('update', $field);
|
||||
|
||||
// Check that the field exists - this is mostly related to the demo, where we
|
||||
// rewrite the data every x minutes, so it's possible someone might be disassociating
|
||||
// a field from a fieldset just as we're wiping the database
|
||||
@@ -157,11 +156,12 @@ class CustomFieldsController extends Controller
|
||||
return redirect()->route('fieldsets.show', ['fieldset' => $fieldset_id])
|
||||
->with('success', trans('admin/custom_fields/message.field.delete.success'));
|
||||
} else {
|
||||
return redirect()->back()->withErrors(['message' => "Field is in use and cannot be deleted."]);
|
||||
return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.error'))
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->back()->withErrors(['message' => "Error deleting field from fieldset"]);
|
||||
return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.error'));
|
||||
|
||||
|
||||
}
|
||||
@@ -172,20 +172,16 @@ class CustomFieldsController extends Controller
|
||||
* @author [Brady Wetherington] [<uberbrady@gmail.com>]
|
||||
* @since [v1.8]
|
||||
*/
|
||||
public function destroy($field_id) : RedirectResponse
|
||||
public function destroy(CustomField $field) : RedirectResponse
|
||||
{
|
||||
if ($field = CustomField::find($field_id)) {
|
||||
$this->authorize('delete', $field);
|
||||
$this->authorize('delete', CustomField::class);
|
||||
|
||||
if (($field->fieldset) && ($field->fieldset->count() > 0)) {
|
||||
return redirect()->back()->withErrors(['message' => 'Field is in-use']);
|
||||
}
|
||||
$field->delete();
|
||||
return redirect()->route("fields.index")
|
||||
->with("success", trans('admin/custom_fields/message.field.delete.success'));
|
||||
if (($field->fieldset) && ($field->fieldset->count() > 0)) {
|
||||
return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.in_use'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withErrors(['message' => 'Field does not exist']);
|
||||
$field->delete();
|
||||
return redirect()->route("fields.index")
|
||||
->with("success", trans('admin/custom_fields/message.field.delete.success'));
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +194,7 @@ class CustomFieldsController extends Controller
|
||||
*/
|
||||
public function edit(Request $request, CustomField $field) : View | RedirectResponse
|
||||
{
|
||||
$this->authorize('update', $field);
|
||||
$this->authorize('update', CustomField::class);
|
||||
$fieldsets = CustomFieldset::get();
|
||||
$customFormat = '';
|
||||
if ((stripos($field->format, 'regex') === 0) && ($field->format !== CustomField::PREDEFINED_FORMATS['MAC'])) {
|
||||
@@ -228,7 +224,7 @@ class CustomFieldsController extends Controller
|
||||
*/
|
||||
public function update(CustomFieldRequest $request, CustomField $field) : RedirectResponse
|
||||
{
|
||||
$this->authorize('update', $field);
|
||||
$this->authorize('update', CustomField::class);
|
||||
$show_in_email = $request->get("show_in_email", 0);
|
||||
$display_in_user_view = $request->get("display_in_user_view", 0);
|
||||
|
||||
@@ -265,7 +261,6 @@ class CustomFieldsController extends Controller
|
||||
|
||||
if ($field->save()) {
|
||||
|
||||
|
||||
// Sync fields with fieldsets
|
||||
$fieldset_array = $request->input('associate_fieldsets');
|
||||
if ($request->has('associate_fieldsets') && (is_array($fieldset_array))) {
|
||||
|
||||
@@ -65,6 +65,7 @@ class GroupsController extends Controller
|
||||
$group->notes = $request->input('notes');
|
||||
|
||||
if ($group->save()) {
|
||||
$group->users()->sync($request->input('associated_users'));
|
||||
return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.create'));
|
||||
}
|
||||
|
||||
@@ -88,7 +89,10 @@ class GroupsController extends Controller
|
||||
$groupPermissions = [];
|
||||
}
|
||||
$selected_array = Helper::selectedPermissionsArray($permissions, $groupPermissions);
|
||||
return view('groups.edit', compact('group', 'permissions', 'selected_array', 'groupPermissions'));
|
||||
$associated_users = $group->users()->get();
|
||||
|
||||
//dd($associated_users->toArray());
|
||||
return view('groups.edit', compact('group', 'permissions', 'selected_array', 'groupPermissions'))->with('associated_users', $associated_users);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,8 +109,10 @@ class GroupsController extends Controller
|
||||
$group->permissions = json_encode($request->input('permission'));
|
||||
$group->notes = $request->input('notes');
|
||||
|
||||
|
||||
if (! config('app.lock_passwords')) {
|
||||
if ($group->save()) {
|
||||
$group->users()->sync($request->input('associated_users'));
|
||||
return redirect()->route('groups.index')->with('success', trans('admin/groups/message.success.update'));
|
||||
}
|
||||
|
||||
|
||||
@@ -64,12 +64,7 @@ class LicenseCheckinController extends Controller
|
||||
|
||||
$this->authorize('checkout', $license);
|
||||
|
||||
if (! $license->reassignable) {
|
||||
// Not allowed to checkin
|
||||
Session::flash('error', trans('admin/licenses/message.checkin.not_reassignable') . '.');
|
||||
|
||||
return redirect()->back()->withInput();
|
||||
}
|
||||
|
||||
// Declare the rules for the form validation
|
||||
$rules = [
|
||||
@@ -86,8 +81,10 @@ class LicenseCheckinController extends Controller
|
||||
}
|
||||
|
||||
if($licenseSeat->assigned_to != null){
|
||||
$return_to = User::find($licenseSeat->assigned_to);
|
||||
session()->put('checkedInFrom', $return_to->id);
|
||||
$return_to = User::withTrashed()->find($licenseSeat->assigned_to);
|
||||
if ($return_to) {
|
||||
session()->put('checkedInFrom', $return_to->id);
|
||||
}
|
||||
} else {
|
||||
$return_to = Asset::find($licenseSeat->asset_id);
|
||||
}
|
||||
@@ -96,16 +93,22 @@ class LicenseCheckinController extends Controller
|
||||
$licenseSeat->assigned_to = null;
|
||||
$licenseSeat->asset_id = null;
|
||||
$licenseSeat->notes = $request->input('notes');
|
||||
if (! $licenseSeat->license->reassignable) {
|
||||
$licenseSeat->unreassignable_seat = true;
|
||||
}
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
|
||||
if ($request->get('redirect_option') === 'target'){
|
||||
session()->put(['checkout_to_type' => 'user']);
|
||||
}
|
||||
|
||||
// Was the asset updated?
|
||||
if ($licenseSeat->save()) {
|
||||
event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $request->input('notes')));
|
||||
event(new CheckoutableCheckedIn($licenseSeat, $return_to, auth()->user(), $licenseSeat->notes));
|
||||
|
||||
|
||||
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.checkin.success'));
|
||||
return Helper::getRedirectOption($request, $license->id, 'Licenses')
|
||||
->with('success', trans('admin/licenses/message.checkin.success'));
|
||||
}
|
||||
|
||||
// Redirect to the license page with error
|
||||
@@ -127,21 +130,17 @@ class LicenseCheckinController extends Controller
|
||||
$license = License::findOrFail($licenseId);
|
||||
$this->authorize('checkin', $license);
|
||||
|
||||
if (! $license->reassignable) {
|
||||
// Not allowed to checkin
|
||||
Session::flash('error', 'License not reassignable.');
|
||||
|
||||
return redirect()->back()->withInput();
|
||||
}
|
||||
|
||||
$licenseSeatsByUser = LicenseSeat::where('license_id', '=', $licenseId)
|
||||
->whereNotNull('assigned_to')
|
||||
->with('user')
|
||||
->with('user', 'license')
|
||||
->get();
|
||||
|
||||
$license = $licenseSeatsByUser->first()?->license;
|
||||
foreach ($licenseSeatsByUser as $user_seat) {
|
||||
$user_seat->assigned_to = null;
|
||||
|
||||
if ($license && ! $license->reassignable) {
|
||||
$user_seat->unreassignable_seat = true;
|
||||
}
|
||||
if ($user_seat->save()) {
|
||||
Log::debug('Checking in '.$license->name.' from user '.$user_seat->username);
|
||||
$user_seat->logCheckin($user_seat->user, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
|
||||
@@ -154,9 +153,12 @@ class LicenseCheckinController extends Controller
|
||||
->get();
|
||||
|
||||
$count = 0;
|
||||
$license = $licenseSeatsByAsset->first()?->license;
|
||||
foreach ($licenseSeatsByAsset as $asset_seat) {
|
||||
$asset_seat->asset_id = null;
|
||||
|
||||
if ($license && ! $license->reassignable) {
|
||||
$asset_seat->unreassignable_seat = true;
|
||||
}
|
||||
if ($asset_seat->save()) {
|
||||
Log::debug('Checking in '.$license->name.' from asset '.$asset_seat->asset_tag);
|
||||
$asset_seat->logCheckin($asset_seat->asset, trans('admin/licenses/general.bulk.checkin_all.log_msg'));
|
||||
|
||||
@@ -25,7 +25,7 @@ class LicenseCheckoutController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
* @return \Illuminate\Contracts\View\View |\Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function create(License $license)
|
||||
@@ -39,6 +39,16 @@ class LicenseCheckoutController extends Controller
|
||||
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
|
||||
}
|
||||
|
||||
// Make sure the license is expired or terminated
|
||||
if ($license->isInactive()) {
|
||||
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
|
||||
}
|
||||
|
||||
// We don't currently allow checking out licenses to locations, so we'll reset that to user if needed
|
||||
if (session()->get('checkout_to_type') == 'location') {
|
||||
session()->put(['checkout_to_type' => 'user']);
|
||||
}
|
||||
|
||||
// Return the checkout view
|
||||
return view('licenses/checkout', compact('license'));
|
||||
}
|
||||
@@ -65,22 +75,31 @@ class LicenseCheckoutController extends Controller
|
||||
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.not_found'));
|
||||
}
|
||||
|
||||
|
||||
$this->authorize('checkout', $license);
|
||||
|
||||
// Make sure there is at least one available to checkout
|
||||
if ($license->availCount()->count() < 1) {
|
||||
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
|
||||
}
|
||||
|
||||
// Make sure the license is expired or terminated
|
||||
if ($license->isInactive()) {
|
||||
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.license_is_inactive'));
|
||||
}
|
||||
|
||||
$licenseSeat = $this->findLicenseSeatToCheckout($license, $seatId);
|
||||
$licenseSeat->created_by = auth()->id();
|
||||
$licenseSeat->notes = $request->input('notes');
|
||||
|
||||
|
||||
$checkoutMethod = 'checkoutTo'.ucwords(request('checkout_to_type'));
|
||||
|
||||
if ($request->filled('asset_id')) {
|
||||
|
||||
session()->put(['checkout_to_type' => 'asset']);
|
||||
$checkoutTarget = $this->checkoutToAsset($licenseSeat);
|
||||
$request->request->add(['assigned_asset' => $checkoutTarget->id]);
|
||||
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'asset']);
|
||||
|
||||
} elseif ($request->filled('assigned_to')) {
|
||||
session()->put(['checkout_to_type' => 'user']);
|
||||
$checkoutTarget = $this->checkoutToUser($licenseSeat);
|
||||
$request->request->add(['assigned_user' => $checkoutTarget->id]);
|
||||
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => 'user']);
|
||||
@@ -89,7 +108,9 @@ class LicenseCheckoutController extends Controller
|
||||
|
||||
|
||||
if ($checkoutTarget) {
|
||||
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.checkout.success'));
|
||||
|
||||
return Helper::getRedirectOption($request, $license->id, 'Licenses')
|
||||
->with('success', trans('admin/licenses/message.checkout.success'));
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +130,7 @@ class LicenseCheckoutController extends Controller
|
||||
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats')));
|
||||
}
|
||||
|
||||
|
||||
if (! $licenseSeat->license->is($license)) {
|
||||
throw new \Illuminate\Http\Exceptions\HttpResponseException(redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.mismatch')));
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Licenses;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\License;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class LicenseFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Validates and stores files associated with a license.
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
* @param int $licenseId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @todo Switch to using the AssetFileRequest form request validator.
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $licenseId = null)
|
||||
{
|
||||
$license = License::find($licenseId);
|
||||
|
||||
if (isset($license->id)) {
|
||||
$this->authorize('update', $license);
|
||||
|
||||
if ($request->hasFile('file')) {
|
||||
if (! Storage::exists('private_uploads/licenses')) {
|
||||
Storage::makeDirectory('private_uploads/licenses', 775);
|
||||
}
|
||||
|
||||
foreach ($request->file('file') as $file) {
|
||||
$file_name = $request->handleFile('private_uploads/licenses/','license-'.$license->id, $file);
|
||||
|
||||
//Log the upload to the log
|
||||
$license->logUpload($file_name, e($request->input('notes')));
|
||||
}
|
||||
|
||||
|
||||
return redirect()->route('licenses.show', $license->id)->with('success', trans('admin/licenses/message.upload.success'));
|
||||
|
||||
}
|
||||
|
||||
return redirect()->route('licenses.show', $license->id)->with('error', trans('admin/licenses/message.upload.nofiles'));
|
||||
}
|
||||
// Prepare the error message
|
||||
return redirect()->route('licenses.index')
|
||||
->with('error', trans('admin/licenses/message.does_not_exist'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the selected license file.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @param int $licenseId
|
||||
* @param int $fileId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function destroy($licenseId = null, $fileId = null)
|
||||
{
|
||||
if ($license = License::find($licenseId)) {
|
||||
|
||||
$this->authorize('update', $license);
|
||||
|
||||
if ($log = Actionlog::find($fileId)) {
|
||||
|
||||
// Remove the file if one exists
|
||||
if (Storage::exists('licenses/'.$log->filename)) {
|
||||
try {
|
||||
Storage::delete('licenses/'.$log->filename);
|
||||
} catch (\Exception $e) {
|
||||
Log::debug($e);
|
||||
}
|
||||
}
|
||||
|
||||
$log->delete();
|
||||
|
||||
return redirect()->back()
|
||||
->with('success', trans('admin/hardware/message.deletefile.success'));
|
||||
}
|
||||
|
||||
return redirect()->route('licenses.index')->with('error', trans('general.log_does_not_exist'));
|
||||
}
|
||||
|
||||
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.does_not_exist'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the selected file to be viewed.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.4]
|
||||
* @param int $licenseId
|
||||
* @param int $fileId
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function show($licenseId = null, $fileId = null, $download = true)
|
||||
{
|
||||
$license = License::find($licenseId);
|
||||
|
||||
// the license is valid
|
||||
if (isset($license->id)) {
|
||||
$this->authorize('view', $license);
|
||||
$this->authorize('licenses.files', $license);
|
||||
|
||||
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $license->id)->find($fileId)) {
|
||||
$file = 'private_uploads/licenses/'.$log->filename;
|
||||
|
||||
try {
|
||||
return StorageHelper::showOrDownloadFile($file, $log->filename);
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->route('licenses.show', ['licenses' => $license])->with('error', trans('general.file_not_found'));
|
||||
}
|
||||
}
|
||||
|
||||
// The log record doesn't exist somehow
|
||||
return redirect()->route('licenses.show', ['licenses' => $license])->with('error', trans('general.log_record_not_found'));
|
||||
|
||||
}
|
||||
|
||||
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.does_not_exist', ['id' => $fileId]));
|
||||
}
|
||||
}
|
||||
@@ -102,10 +102,15 @@ class LicensesController extends Controller
|
||||
$license->created_by = auth()->id();
|
||||
$license->min_amt = $request->input('min_amt');
|
||||
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
if($request->get('redirect_option') === 'back'){
|
||||
session()->put(['redirect_option' => 'index']);
|
||||
} else {
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
}
|
||||
|
||||
if ($license->save()) {
|
||||
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.create.success'));
|
||||
return Helper::getRedirectOption($request, $license->id, 'Licenses')
|
||||
->with('success', trans('admin/licenses/message.create.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->withErrors($license->getErrors());
|
||||
@@ -125,7 +130,7 @@ class LicensesController extends Controller
|
||||
{
|
||||
|
||||
$this->authorize('update', $license);
|
||||
|
||||
session()->put('back_url', url()->previous());
|
||||
$maintained_list = [
|
||||
'' => 'Maintained',
|
||||
'1' => 'Yes',
|
||||
@@ -181,7 +186,8 @@ class LicensesController extends Controller
|
||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||
|
||||
if ($license->save()) {
|
||||
return redirect()->to(Helper::getRedirectOption($request, $license->id, 'Licenses'))->with('success', trans('admin/licenses/message.update.success'));
|
||||
return Helper::getRedirectOption($request, $license->id, 'Licenses')
|
||||
->with('success', trans('admin/licenses/message.update.success'));
|
||||
}
|
||||
// If we can't adjust the number of seats, the error is flashed to the session by the event handler in License.php
|
||||
return redirect()->back()->withInput()->withErrors($license->getErrors());
|
||||
@@ -239,16 +245,28 @@ class LicensesController extends Controller
|
||||
$license = License::with('assignedusers')->find($license->id);
|
||||
|
||||
$users_count = User::where('autoassign_licenses', '1')->count();
|
||||
$total_seats_count = $license->totalSeatsByLicenseID();
|
||||
|
||||
$total_seats_count = (int) $license->totalSeatsByLicenseID();
|
||||
$available_seats_count = $license->availCount()->count();
|
||||
$checkedout_seats_count = ($total_seats_count - $available_seats_count);
|
||||
$unreassignable_seats_count = License::unReassignableCount($license);
|
||||
|
||||
if(!$license->reassignable){
|
||||
$checkedout_seats_count = ($total_seats_count - $available_seats_count - $unreassignable_seats_count );
|
||||
}
|
||||
else {
|
||||
$checkedout_seats_count = ($total_seats_count - $available_seats_count);
|
||||
}
|
||||
if($license->isInactive()){
|
||||
session()->flash('warning', (trans('admin/licenses/message.checkout.license_is_inactive')));
|
||||
}
|
||||
|
||||
$this->authorize('view', $license);
|
||||
return view('licenses.view', compact('license'))
|
||||
->with('users_count', $users_count)
|
||||
->with('total_seats_count', $total_seats_count)
|
||||
->with('available_seats_count', $available_seats_count)
|
||||
->with('checkedout_seats_count', $checkedout_seats_count);
|
||||
->with('checkedout_seats_count', $checkedout_seats_count)
|
||||
->with('unreassignable_seats_count', $unreassignable_seats_count);
|
||||
|
||||
}
|
||||
|
||||
@@ -302,13 +320,16 @@ class LicensesController extends Controller
|
||||
$response = new StreamedResponse(function () {
|
||||
// Open output stream
|
||||
$handle = fopen('php://output', 'w');
|
||||
$licenses= License::with('company',
|
||||
$licenses = License::with('company',
|
||||
'manufacturer',
|
||||
'category',
|
||||
'supplier',
|
||||
'adminuser',
|
||||
'assignedusers')
|
||||
->orderBy('created_at', 'DESC');
|
||||
'assignedusers');
|
||||
if (request()->filled('category_id')) {
|
||||
$licenses = $licenses->where('category_id', request()->input('category_id'));
|
||||
}
|
||||
$licenses = $licenses->orderBy('created_at', 'DESC');
|
||||
Company::scopeCompanyables($licenses)
|
||||
->chunk(500, function ($licenses) use ($handle) {
|
||||
$headers = [
|
||||
@@ -355,7 +376,7 @@ class LicensesController extends Controller
|
||||
$license->order_number,
|
||||
$license->free_seat_count,
|
||||
$license->seats,
|
||||
($license->adminuser ? $license->adminuser->present()->fullName() : trans('admin/reports/general.deleted_user')),
|
||||
($license->adminuser ? $license->adminuser->display_name : trans('admin/reports/general.deleted_user')),
|
||||
$license->depreciation ? $license->depreciation->name: '',
|
||||
$license->updated_at,
|
||||
$license->deleted_at,
|
||||
|
||||
@@ -96,7 +96,18 @@ class LocationsController extends Controller
|
||||
$location->company_id = $request->input('company_id');
|
||||
}
|
||||
|
||||
$location = $request->handleImages($location);
|
||||
if ($request->has('use_cloned_image')) {
|
||||
$cloned_model_img = Location::select('image')->find($request->input('clone_image_from_id'));
|
||||
if ($cloned_model_img) {
|
||||
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
|
||||
$new_image = 'locations/'.$new_image_name;
|
||||
Storage::disk('public')->copy('locations/'.$cloned_model_img->image, $new_image);
|
||||
$location->image = $new_image_name;
|
||||
}
|
||||
|
||||
} else {
|
||||
$location = $request->handleImages($location);
|
||||
}
|
||||
|
||||
if ($location->save()) {
|
||||
return redirect()->route('locations.index')->with('success', trans('admin/locations/message.create.success'));
|
||||
@@ -178,30 +189,36 @@ class LocationsController extends Controller
|
||||
{
|
||||
$this->authorize('delete', Location::class);
|
||||
|
||||
if (is_null($location = Location::find($locationId))) {
|
||||
$location = Location::withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('assignedAccessories as assigned_accessories_count')
|
||||
->withCount('accessories as accessories_count')
|
||||
->withCount('rtd_assets as rtd_assets_count')
|
||||
->withCount('children as children_count')
|
||||
->withCount('users as users_count')
|
||||
->withCount('consumables as consumables_count')
|
||||
->withCount('components as components_count')
|
||||
->find($locationId);
|
||||
|
||||
if (!$location) {
|
||||
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.does_not_exist'));
|
||||
}
|
||||
|
||||
if ($location->users()->count() > 0) {
|
||||
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_users'));
|
||||
} elseif ($location->children()->count() > 0) {
|
||||
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_child_loc'));
|
||||
} elseif ($location->assets()->count() > 0) {
|
||||
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_assets'));
|
||||
} elseif ($location->assignedassets()->count() > 0) {
|
||||
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_assets'));
|
||||
}
|
||||
if ($location->isDeletable()) {
|
||||
|
||||
if ($location->image) {
|
||||
try {
|
||||
Storage::disk('public')->delete('locations/'.$location->image);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
if ($location->image) {
|
||||
try {
|
||||
Storage::disk('public')->delete('locations/'.$location->image);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
}
|
||||
$location->delete();
|
||||
return redirect()->to(route('locations.index'))->with('success', trans('admin/locations/message.delete.success'));
|
||||
} else {
|
||||
return redirect()->to(route('locations.index'))->with('error', trans('admin/locations/message.assoc_users'));
|
||||
}
|
||||
$location->delete();
|
||||
|
||||
return redirect()->to(route('locations.index'))->with('success', trans('admin/locations/message.delete.success'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,23 +253,41 @@ class LocationsController extends Controller
|
||||
$this->authorize('view', Location::class);
|
||||
|
||||
if ($location = Location::where('id', $id)->first()) {
|
||||
$parent = Location::where('id', $location->parent_id)->first();
|
||||
$manager = User::where('id', $location->manager_id)->first();
|
||||
$company = Company::where('id', $location->company_id)->first();
|
||||
$users = User::where('location_id', $id)->with('company', 'department', 'location')->get();
|
||||
$assets = Asset::where('assigned_to', $id)->where('assigned_type', Location::class)->with('model', 'model.category')->get();
|
||||
return view('locations/print')
|
||||
->with('assets', $assets)
|
||||
->with('users',$users)
|
||||
->with('assigned', false)
|
||||
->with('assets', $location->assets)
|
||||
->with('assignedAssets', $location->assignedAssets)
|
||||
->with('accessories', $location->accessories)
|
||||
->with('assignedAccessories', $location->assignedAccessories)
|
||||
->with('users',$location->users)
|
||||
->with('location', $location)
|
||||
->with('parent', $parent)
|
||||
->with('manager', $manager)
|
||||
->with('company', $company);
|
||||
->with('consumables', $location->consumables)
|
||||
->with('components', $location->components)
|
||||
->with('children', $location->children);
|
||||
}
|
||||
|
||||
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
|
||||
}
|
||||
|
||||
public function print_all_assigned($id) : View | RedirectResponse
|
||||
{
|
||||
$this->authorize('view', Location::class);
|
||||
if ($location = Location::where('id', $id)->first()) {
|
||||
return view('locations/print')
|
||||
->with('assigned', true)
|
||||
->with('assets', $location->assets)
|
||||
->with('assignedAssets', $location->assignedAssets)
|
||||
->with('accessories', $location->accessories)
|
||||
->with('assignedAccessories', $location->assignedAccessories)
|
||||
->with('users',$location->users)
|
||||
->with('location', $location)
|
||||
->with('consumables', $location->consumables)
|
||||
->with('components', $location->components)
|
||||
->with('children', $location->children);
|
||||
}
|
||||
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a view that presents a form to clone a location.
|
||||
@@ -275,9 +310,9 @@ class LocationsController extends Controller
|
||||
|
||||
// unset these values
|
||||
$location->id = null;
|
||||
$location->image = null;
|
||||
|
||||
return view('locations/edit')
|
||||
->with('cloned_model', $location_to_clone)
|
||||
->with('item', $location);
|
||||
}
|
||||
|
||||
@@ -310,33 +345,12 @@ class LocationsController extends Controller
|
||||
return redirect()->route('locations.index')->with('success', trans('admin/locations/message.restore.success'));
|
||||
}
|
||||
|
||||
// Check validation
|
||||
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.location'), 'error' => $location->getErrors()->first()]));
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
|
||||
|
||||
}
|
||||
public function print_all_assigned($id) : View | RedirectResponse
|
||||
{
|
||||
$this->authorize('view', Location::class);
|
||||
if ($location = Location::where('id', $id)->first()) {
|
||||
$parent = Location::where('id', $location->parent_id)->first();
|
||||
$manager = User::where('id', $location->manager_id)->first();
|
||||
$company = Company::where('id', $location->company_id)->first();
|
||||
$users = User::where('location_id', $id)->with('company', 'department', 'location')->get();
|
||||
$assets = Asset::where('location_id', $id)->with('model', 'model.category')->get();
|
||||
return view('locations/print')
|
||||
->with('assets', $assets)
|
||||
->with('users',$users)
|
||||
->with('location', $location)
|
||||
->with('parent', $parent)
|
||||
->with('manager', $manager)
|
||||
->with('company', $company);
|
||||
}
|
||||
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a view that allows the user to bulk delete locations
|
||||
@@ -355,8 +369,12 @@ class LocationsController extends Controller
|
||||
$locations = Location::whereIn('id', $locations_raw_array)
|
||||
->withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('assignedAccessories as assigned_accessories_count')
|
||||
->withCount('accessories as accessories_count')
|
||||
->withCount('rtd_assets as rtd_assets_count')
|
||||
->withCount('children as children_count')
|
||||
->withCount('consumables as consumables_count')
|
||||
->withCount('components as components_count')
|
||||
->withCount('users as users_count')->get();
|
||||
|
||||
$valid_count = 0;
|
||||
@@ -389,9 +407,13 @@ class LocationsController extends Controller
|
||||
$locations = Location::whereIn('id', $locations_raw_array)
|
||||
->withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('assignedAccessories as assigned_accessories_count')
|
||||
->withCount('accessories as accessories_count')
|
||||
->withCount('rtd_assets as rtd_assets_count')
|
||||
->withCount('children as children_count')
|
||||
->withCount('users as users_count')->get();
|
||||
->withCount('users as users_count')
|
||||
->withCount('consumables as consumables_count')
|
||||
->withCount('components as components_count')->get();
|
||||
|
||||
$success_count = 0;
|
||||
$error_count = 0;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user