Compare commits
1 Commits
form-row-c
...
fix_s3_bac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93014d7f34 |
@@ -2590,10 +2590,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"login": "qveensi",
|
"login": "QveenSi",
|
||||||
"name": "Yevhenii Huzii",
|
"name": "Yevhenii Huzii",
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/19945501?v=4",
|
"avatar_url": "https://avatars.githubusercontent.com/u/19945501?v=4",
|
||||||
"profile": "https://github.com/qveensi",
|
"profile": "https://github.com/QveenSi",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
@@ -2607,6 +2607,15 @@
|
|||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"login": "QveenSi",
|
||||||
|
"name": "Yevhenii Huzii",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/19945501?v=4",
|
||||||
|
"profile": "https://github.com/QveenSi",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"login": "chrisweirich",
|
"login": "chrisweirich",
|
||||||
"name": "Christian Weirich",
|
"name": "Christian Weirich",
|
||||||
@@ -3206,8 +3215,7 @@
|
|||||||
"avatar_url": "https://avatars.githubusercontent.com/u/3755203?v=4",
|
"avatar_url": "https://avatars.githubusercontent.com/u/3755203?v=4",
|
||||||
"profile": "https://github.com/swift2512",
|
"profile": "https://github.com/swift2512",
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"bug",
|
"bug"
|
||||||
"code"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -3290,951 +3298,6 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Fiala06",
|
|
||||||
"name": "Fiala06",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/5183146?v=4",
|
|
||||||
"profile": "https://github.com/Fiala06",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "ntaylor-86",
|
|
||||||
"name": "Nathan Taylor",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/28693782?v=4",
|
|
||||||
"profile": "https://github.com/ntaylor-86",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "fvollmer",
|
|
||||||
"name": "fvollmer",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/16699443?v=4",
|
|
||||||
"profile": "https://github.com/fvollmer",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "36864",
|
|
||||||
"name": "36864",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/109086466?v=4",
|
|
||||||
"profile": "https://github.com/36864",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "CloCkWeRX",
|
|
||||||
"name": "Daniel O'Connor",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/365751?v=4",
|
|
||||||
"profile": "http://clockwerx.blogspot.com/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "BeatSpark",
|
|
||||||
"name": "BeatSpark",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/102852568?v=4",
|
|
||||||
"profile": "https://github.com/BeatSpark",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mrdahbi",
|
|
||||||
"name": "mrdahbi",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/59203607?v=4",
|
|
||||||
"profile": "https://github.com/mrdahbi",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "chfsx",
|
|
||||||
"name": "Fabian Schmid",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/6661332?v=4",
|
|
||||||
"profile": "http://sr.solutions",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "realchrisolin",
|
|
||||||
"name": "Chris Olin",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1288116?v=4",
|
|
||||||
"profile": "https://www.chrisolin.com",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mnemonicly",
|
|
||||||
"name": "Dan",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/3803132?v=4",
|
|
||||||
"profile": "https://github.com/mnemonicly",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "NebelKreis",
|
|
||||||
"name": "Nebel",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/43917728?v=4",
|
|
||||||
"profile": "https://github.com/NebelKreis",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "test1337ahp",
|
|
||||||
"name": "test1337ahp",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/132433803?v=4",
|
|
||||||
"profile": "https://github.com/test1337ahp",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "JonathonReinhart",
|
|
||||||
"name": "Jonathon Reinhart",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1916566?v=4",
|
|
||||||
"profile": "https://github.com/JonathonReinhart",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "aranar-pro",
|
|
||||||
"name": "aranar-pro",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/484742?v=4",
|
|
||||||
"profile": "https://github.com/aranar-pro",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "phil-flip",
|
|
||||||
"name": "Phil",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/27019397?v=4",
|
|
||||||
"profile": "https://github.com/phil-flip",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "fe80",
|
|
||||||
"name": "Steffy Fort",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/6473460?v=4",
|
|
||||||
"profile": "https://fe80.fr/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "sorvani",
|
|
||||||
"name": "Jared Busch",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/3302372?v=4",
|
|
||||||
"profile": "https://github.com/sorvani",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "seanborg-codethink",
|
|
||||||
"name": "seanborg-codethink",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/111956991?v=4",
|
|
||||||
"profile": "https://github.com/seanborg-codethink",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "dkaatz",
|
|
||||||
"name": "dkaatz",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/160669961?v=4",
|
|
||||||
"profile": "https://github.com/dkaatz",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "DanielRuf",
|
|
||||||
"name": "Daniel Ruf",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/827205?v=4",
|
|
||||||
"profile": "https://threema.id/74SF7MW6?text=",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "ahpaleus",
|
|
||||||
"name": "ahpaleus",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/38883201?v=4",
|
|
||||||
"profile": "https://github.com/ahpaleus",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mink-adao-duy",
|
|
||||||
"name": "Anh DAO-DUY",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/22906055?v=4",
|
|
||||||
"profile": "https://github.com/mink-adao-duy",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Serdnad",
|
|
||||||
"name": "Andres Gutierrez",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/4723453?v=4",
|
|
||||||
"profile": "https://github.com/Serdnad",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "wewhite",
|
|
||||||
"name": "Warren White",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/111083379?v=4",
|
|
||||||
"profile": "https://github.com/wewhite",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "robintemme",
|
|
||||||
"name": "Robin Temme",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/2809241?v=4",
|
|
||||||
"profile": "https://robintemme.de/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "herroworrd",
|
|
||||||
"name": "herroworrd",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/47008367?v=4",
|
|
||||||
"profile": "https://github.com/herroworrd",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "vicleos",
|
|
||||||
"name": "vicleos",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/28558609?v=4",
|
|
||||||
"profile": "https://mubiu.com/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "thinkl33t",
|
|
||||||
"name": "Bob Clough",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1016780?v=4",
|
|
||||||
"profile": "http://thinkl33t.co.uk/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "brandon-bailey",
|
|
||||||
"name": "Brandon Daniel Bailey",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/10648463?v=4",
|
|
||||||
"profile": "https://github.com/brandon-bailey",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "marcquark",
|
|
||||||
"name": "Marc Bartelt",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/23556080?v=4",
|
|
||||||
"profile": "https://github.com/marcquark",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "manu-crealytics",
|
|
||||||
"name": "manu-crealytics",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/18286893?v=4",
|
|
||||||
"profile": "https://github.com/manu-crealytics",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Galaxy102",
|
|
||||||
"name": "Konstantin Köhring",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/18245993?v=4",
|
|
||||||
"profile": "https://www.galaxy102.de/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "deloz",
|
|
||||||
"name": "Deloz",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/685167?v=4",
|
|
||||||
"profile": "https://deloz.net/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mbrrg",
|
|
||||||
"name": "Martin Berg",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/2682426?v=4",
|
|
||||||
"profile": "https://github.com/mbrrg",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Nothing4You",
|
|
||||||
"name": "Richard Schwab",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/3694534?v=4",
|
|
||||||
"profile": "https://github.com/Nothing4You",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "rickheil",
|
|
||||||
"name": "Rick Heil",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8959676?v=4",
|
|
||||||
"profile": "https://rickheil.com/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "rosscdh",
|
|
||||||
"name": "Ross Crawford-d'Heureuse",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/397106?v=4",
|
|
||||||
"profile": "https://github.com/rosscdh",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "McG800",
|
|
||||||
"name": "Ryan McGuire",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1621107?v=4",
|
|
||||||
"profile": "https://github.com/McG800",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "SBrown2021",
|
|
||||||
"name": "SBrown2021",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/77835667?v=4",
|
|
||||||
"profile": "https://github.com/SBrown2021",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "serkanerip",
|
|
||||||
"name": "Serkan",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8780913?v=4",
|
|
||||||
"profile": "https://github.com/serkanerip",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Shankschn",
|
|
||||||
"name": "Shanks",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/63188620?v=4",
|
|
||||||
"profile": "https://www.yudelei.com/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "cendai-mis",
|
|
||||||
"name": "cendai-mis",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/198525698?v=4",
|
|
||||||
"profile": "https://github.com/cendai-mis",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "smcpeck",
|
|
||||||
"name": "Shaun McPeck",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8724583?v=4",
|
|
||||||
"profile": "https://smcpeck.github.io/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "snazy2000",
|
|
||||||
"name": "Stephen",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1378836?v=4",
|
|
||||||
"profile": "https://github.com/snazy2000",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Nevets82",
|
|
||||||
"name": "Steven",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/4462739?v=4",
|
|
||||||
"profile": "http://nevets82.github.io/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Mateus-Romera",
|
|
||||||
"name": "Mateus Villar",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/29017267?v=4",
|
|
||||||
"profile": "https://mateusvillar.com/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mzack5020",
|
|
||||||
"name": "Matthew Zackschewski",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/12749393?v=4",
|
|
||||||
"profile": "https://github.com/mzack5020",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "firefrei",
|
|
||||||
"name": "Matthias Frei",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/12660103?v=4",
|
|
||||||
"profile": "https://www.frei.media/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "nticaric",
|
|
||||||
"name": "Nenad Ticaric",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/824840?v=4",
|
|
||||||
"profile": "https://github.com/nticaric",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Scorcher",
|
|
||||||
"name": "Nikolay Didenko",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/706439?v=4",
|
|
||||||
"profile": "https://github.com/Scorcher",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "nunomaduro",
|
|
||||||
"name": "Nuno Maduro",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/5457236?v=4",
|
|
||||||
"profile": "https://nunomaduro.com/sponsorships",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "owalerys",
|
|
||||||
"name": "Oliver Walerys",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8883074?v=4",
|
|
||||||
"profile": "https://tektikhq.com/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "rcmcdonald91",
|
|
||||||
"name": "R. Christian McDonald",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/3102039?v=4",
|
|
||||||
"profile": "https://keybase.io/rcmcdonald91",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "nixn",
|
|
||||||
"name": "nix",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1525581?v=4",
|
|
||||||
"profile": "https://nnix.net/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "octobunny",
|
|
||||||
"name": "octobunny",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/55462380?v=4",
|
|
||||||
"profile": "https://github.com/octobunny",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "sreyemnayr",
|
|
||||||
"name": "Ryan",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8558670?v=4",
|
|
||||||
"profile": "https://github.com/sreyemnayr",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "p3nj",
|
|
||||||
"name": "p3nj",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1501022?v=4",
|
|
||||||
"profile": "https://benji.ltd/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "timwsuqld",
|
|
||||||
"name": "Tim White",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/6201617?v=4",
|
|
||||||
"profile": "https://github.com/timwsuqld",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "yannikp",
|
|
||||||
"name": "yannikp",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/22473767?v=4",
|
|
||||||
"profile": "https://github.com/yannikp",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "viclou",
|
|
||||||
"name": "victoria",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/20525448?v=4",
|
|
||||||
"profile": "https://github.com/viclou",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "valentyntu",
|
|
||||||
"name": "Valentyn Tulub",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/40685314?v=4",
|
|
||||||
"profile": "https://github.com/valentyntu",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Wouter0100",
|
|
||||||
"name": "Wouter van Os",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/864520?v=4",
|
|
||||||
"profile": "http://wouter0100.nl/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "xWyatt",
|
|
||||||
"name": "Wyatt Teeter",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/3946540?v=4",
|
|
||||||
"profile": "https://www.linkedin.com/in/wyatt-teeter",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "terwey",
|
|
||||||
"name": "Yorick Terweijden",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1596124?v=4",
|
|
||||||
"profile": "https://github.com/terwey",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "bmkalle",
|
|
||||||
"name": "bmkalle",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/69298836?v=4",
|
|
||||||
"profile": "https://github.com/bmkalle",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "bricelabelle",
|
|
||||||
"name": "bricelabelle",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/28403467?v=4",
|
|
||||||
"profile": "https://github.com/bricelabelle",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "corydlamb",
|
|
||||||
"name": "corydlamb",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/97770090?v=4",
|
|
||||||
"profile": "https://github.com/corydlamb",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "splashx",
|
|
||||||
"name": "Diogenes S. Jesus",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1154133?v=4",
|
|
||||||
"profile": "http://twitter.com/splash",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "dkmansion",
|
|
||||||
"name": "D M",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/5826629?v=4",
|
|
||||||
"profile": "https://github.com/dkmansion",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Jarli01",
|
|
||||||
"name": "Dustin B",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/14837699?v=4",
|
|
||||||
"profile": "https://github.com/Jarli01",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "fabiang",
|
|
||||||
"name": "Fabian Grutschus",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/348344?v=4",
|
|
||||||
"profile": "https://github.com/fabiang",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "MelonSmasher",
|
|
||||||
"name": "MelonSmasher",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1491053?v=4",
|
|
||||||
"profile": "https://github.com/MelonSmasher",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "AlexanderWPapyrus",
|
|
||||||
"name": "AlexanderWPapyrus",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/80526133?v=4",
|
|
||||||
"profile": "https://github.com/AlexanderWPapyrus",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "disc",
|
|
||||||
"name": "Alexandr Hacicheant",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/306231?v=4",
|
|
||||||
"profile": "https://github.com/disc",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "hex128",
|
|
||||||
"name": "Hex",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/3032891?v=4",
|
|
||||||
"profile": "https://hex128.io/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "arukompas",
|
|
||||||
"name": "Arunas Skirius",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8697942?v=4",
|
|
||||||
"profile": "https://github.com/arukompas",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "benperiton",
|
|
||||||
"name": "Ben Periton",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/104396?v=4",
|
|
||||||
"profile": "https://github.com/benperiton",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "byronwolfman",
|
|
||||||
"name": "Byron Wolfman",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/11906832?v=4",
|
|
||||||
"profile": "https://wolfman.dev/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "CalvinSchwartz",
|
|
||||||
"name": "Calvin",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/56485508?v=4",
|
|
||||||
"profile": "https://github.com/CalvinSchwartz",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "juanfont",
|
|
||||||
"name": "Juan Font",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/181059?v=4",
|
|
||||||
"profile": "https://github.com/juanfont",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "juhotaipale",
|
|
||||||
"name": "Juho Taipale",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/13137708?v=4",
|
|
||||||
"profile": "https://github.com/juhotaipale",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "KorvinSzanto",
|
|
||||||
"name": "Korvin Szanto",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1007419?v=4",
|
|
||||||
"profile": "https://github.com/KorvinSzanto",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "sniff122",
|
|
||||||
"name": "Lewis Foster",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8513053?v=4",
|
|
||||||
"profile": "https://lewisfoster.foo/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "loganswartz",
|
|
||||||
"name": "Logan Swartzendruber",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/33877541?v=4",
|
|
||||||
"profile": "https://github.com/loganswartz",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "lopezio",
|
|
||||||
"name": "Lorenzo P.",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1156208?v=4",
|
|
||||||
"profile": "https://github.com/lopezio",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "m4us1ne",
|
|
||||||
"name": "Lukas Jung",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/33946590?v=4",
|
|
||||||
"profile": "https://github.com/m4us1ne",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "LeafedFox",
|
|
||||||
"name": "Ellie",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/10965027?v=4",
|
|
||||||
"profile": "https://leafedfox.xyz/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "gastamper",
|
|
||||||
"name": "GA Stamper",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/20960555?v=4",
|
|
||||||
"profile": "https://github.com/gastamper",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "gl-pup",
|
|
||||||
"name": "Guillaume Lefranc",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/206553556?v=4",
|
|
||||||
"profile": "https://github.com/gl-pup",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "dasjoe",
|
|
||||||
"name": "Hajo Möller",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/733892?v=4",
|
|
||||||
"profile": "https://github.com/dasjoe",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "pottom",
|
|
||||||
"name": "Istvan Basa",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/3420063?v=4",
|
|
||||||
"profile": "https://github.com/pottom",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "jjasghar",
|
|
||||||
"name": "JJ Asghar",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/810824?v=4",
|
|
||||||
"profile": "https://jjasghar.github.io/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "JemCdo",
|
|
||||||
"name": "James E. Msenga",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/40404495?v=4",
|
|
||||||
"profile": "https://github.com/JemCdo",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "jfwiebe",
|
|
||||||
"name": "Jan Felix Wiebe",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/6865786?v=4",
|
|
||||||
"profile": "https://github.com/jfwiebe",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "drexljo",
|
|
||||||
"name": "Jo Drexl",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/43412008?v=4",
|
|
||||||
"profile": "https://www.nfon.com/",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "austinsasko",
|
|
||||||
"name": "Austin Sasko",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/4807843?v=4",
|
|
||||||
"profile": "https://github.com/austinsasko",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "JassonCordones",
|
|
||||||
"name": "Jasson",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/4875039?v=4",
|
|
||||||
"profile": "http://jassoncordones.github.io",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Tinyblargon",
|
|
||||||
"name": "Okean",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/76069640?v=4",
|
|
||||||
"profile": "https://github.com/Tinyblargon",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "amedranogil",
|
|
||||||
"name": "Alejandro Medrano",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/6515064?v=4",
|
|
||||||
"profile": "https://www.lst.tfo.upm.es/alejandro-medrano/",
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ DB_USERNAME=snipeit
|
|||||||
DB_PASSWORD=changeme1234
|
DB_PASSWORD=changeme1234
|
||||||
DB_PREFIX=null
|
DB_PREFIX=null
|
||||||
DB_DUMP_PATH='/usr/bin'
|
DB_DUMP_PATH='/usr/bin'
|
||||||
DB_DUMP_SKIP_SSL=true
|
|
||||||
DB_CHARSET=utf8mb4
|
DB_CHARSET=utf8mb4
|
||||||
DB_COLLATION=utf8mb4_unicode_ci
|
DB_COLLATION=utf8mb4_unicode_ci
|
||||||
|
|
||||||
@@ -79,13 +78,6 @@ MAIL_BACKUP_NOTIFICATION_DRIVER=null
|
|||||||
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
|
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
|
||||||
BACKUP_ENV=true
|
BACKUP_ENV=true
|
||||||
|
|
||||||
# --------------------------------------------
|
|
||||||
# OPTIONAL: CHANGE PHP UPLOAD LIMITS (UNCOMMENT WHEN NEEDING TO BE CHANGED)
|
|
||||||
# --------------------------------------------
|
|
||||||
#PHP_UPLOAD_LIMIT=10
|
|
||||||
#PHP_POST_MAX_SIZE=10
|
|
||||||
#PHP_UPLOAD_MAX_FILESIZE=10
|
|
||||||
#PHP_MEMORY_LIMIT=10
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# OPTIONAL: SESSION SETTINGS
|
# OPTIONAL: SESSION SETTINGS
|
||||||
|
|||||||
12
.env.docker
12
.env.docker
@@ -28,7 +28,6 @@ PUBLIC_FILESYSTEM_DISK=local_public
|
|||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=db
|
DB_HOST=db
|
||||||
DB_SOCKET=null
|
|
||||||
DB_PORT='3306'
|
DB_PORT='3306'
|
||||||
DB_DATABASE=snipeit
|
DB_DATABASE=snipeit
|
||||||
DB_USERNAME=snipeit
|
DB_USERNAME=snipeit
|
||||||
@@ -36,7 +35,6 @@ DB_PASSWORD=changeme1234
|
|||||||
MYSQL_ROOT_PASSWORD=changeme1234
|
MYSQL_ROOT_PASSWORD=changeme1234
|
||||||
DB_PREFIX=null
|
DB_PREFIX=null
|
||||||
DB_DUMP_PATH='/usr/bin'
|
DB_DUMP_PATH='/usr/bin'
|
||||||
DB_DUMP_SKIP_SSL=true
|
|
||||||
DB_CHARSET=utf8mb4
|
DB_CHARSET=utf8mb4
|
||||||
DB_COLLATION=utf8mb4_unicode_ci
|
DB_COLLATION=utf8mb4_unicode_ci
|
||||||
|
|
||||||
@@ -85,15 +83,6 @@ MAIL_BACKUP_NOTIFICATION_DRIVER=null
|
|||||||
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
|
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
|
||||||
BACKUP_ENV=true
|
BACKUP_ENV=true
|
||||||
|
|
||||||
# --------------------------------------------
|
|
||||||
# OPTIONAL: CHANGE PHP UPLOAD LIMITS (UNCOMMENT WHEN NEEDING TO BE CHANGED)
|
|
||||||
# --------------------------------------------
|
|
||||||
#PHP_UPLOAD_LIMIT=10
|
|
||||||
#PHP_POST_MAX_SIZE=10
|
|
||||||
#PHP_UPLOAD_MAX_FILESIZE=10
|
|
||||||
#PHP_MEMORY_LIMIT=10
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# OPTIONAL: SESSION SETTINGS
|
# OPTIONAL: SESSION SETTINGS
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
@@ -169,7 +158,6 @@ AWS_DEFAULT_REGION=null
|
|||||||
LOGIN_MAX_ATTEMPTS=5
|
LOGIN_MAX_ATTEMPTS=5
|
||||||
LOGIN_LOCKOUT_DURATION=60
|
LOGIN_LOCKOUT_DURATION=60
|
||||||
RESET_PASSWORD_LINK_EXPIRES=900
|
RESET_PASSWORD_LINK_EXPIRES=900
|
||||||
INVITE_PASSWORD_LINK_EXPIRES=1500
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# OPTIONAL: MISC
|
# OPTIONAL: MISC
|
||||||
|
|||||||
13
.env.example
13
.env.example
@@ -24,14 +24,12 @@ PUBLIC_FILESYSTEM_DISK=local_public
|
|||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
DB_CONNECTION=mysql
|
DB_CONNECTION=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_SOCKET=null
|
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_DATABASE=null
|
DB_DATABASE=null
|
||||||
DB_USERNAME=null
|
DB_USERNAME=null
|
||||||
DB_PASSWORD=null
|
DB_PASSWORD=null
|
||||||
DB_PREFIX=null
|
DB_PREFIX=null
|
||||||
DB_DUMP_PATH='/usr/bin'
|
DB_DUMP_PATH='/usr/bin'
|
||||||
DB_DUMP_SKIP_SSL=false
|
|
||||||
DB_CHARSET=utf8mb4
|
DB_CHARSET=utf8mb4
|
||||||
DB_COLLATION=utf8mb4_unicode_ci
|
DB_COLLATION=utf8mb4_unicode_ci
|
||||||
DB_SANITIZE_BY_DEFAULT=false
|
DB_SANITIZE_BY_DEFAULT=false
|
||||||
@@ -175,13 +173,11 @@ LOGIN_AUTOCOMPLETE=false
|
|||||||
RESET_PASSWORD_LINK_EXPIRES=15
|
RESET_PASSWORD_LINK_EXPIRES=15
|
||||||
PASSWORD_CONFIRM_TIMEOUT=10800
|
PASSWORD_CONFIRM_TIMEOUT=10800
|
||||||
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
|
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
|
||||||
INVITE_PASSWORD_LINK_EXPIRES=1500
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# OPTIONAL: MISC
|
# OPTIONAL: MISC
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
LOG_CHANNEL=single
|
LOG_CHANNEL=single
|
||||||
LOG_DEPRECATIONS=false
|
|
||||||
LOG_MAX_DAYS=10
|
LOG_MAX_DAYS=10
|
||||||
APP_LOCKED=false
|
APP_LOCKED=false
|
||||||
APP_CIPHER=AES-256-CBC
|
APP_CIPHER=AES-256-CBC
|
||||||
@@ -190,21 +186,14 @@ APP_ALLOW_INSECURE_HOSTS=false
|
|||||||
GOOGLE_MAPS_API=
|
GOOGLE_MAPS_API=
|
||||||
LDAP_MEM_LIM=500M
|
LDAP_MEM_LIM=500M
|
||||||
LDAP_TIME_LIM=600
|
LDAP_TIME_LIM=600
|
||||||
BACKUP_TIME_LIMIT=600
|
|
||||||
IMPORT_TIME_LIMIT=600
|
IMPORT_TIME_LIMIT=600
|
||||||
IMPORT_MEMORY_LIMIT=500M
|
IMPORT_MEMORY_LIMIT=500M
|
||||||
REPORT_TIME_LIMIT=12000
|
REPORT_TIME_LIMIT=12000
|
||||||
|
REQUIRE_SAML=false
|
||||||
API_THROTTLE_PER_MINUTE=120
|
API_THROTTLE_PER_MINUTE=120
|
||||||
CSV_ESCAPE_FORMULAS=true
|
CSV_ESCAPE_FORMULAS=true
|
||||||
LIVEWIRE_URL_PREFIX=null
|
LIVEWIRE_URL_PREFIX=null
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------
|
|
||||||
# OPTIONAL: SAML SETTINGS
|
|
||||||
# --------------------------------------------
|
|
||||||
REQUIRE_SAML=false
|
|
||||||
SAML_KEY_SIZE=2048
|
|
||||||
|
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
# OPTIONAL: HASHING
|
# OPTIONAL: HASHING
|
||||||
# --------------------------------------------
|
# --------------------------------------------
|
||||||
|
|||||||
163
.github/ISSUE_TEMPLATE/Bug-Report.yml
vendored
163
.github/ISSUE_TEMPLATE/Bug-Report.yml
vendored
@@ -1,163 +0,0 @@
|
|||||||
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
38
.github/ISSUE_TEMPLATE/Feature-Request.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
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' ]
|
language: [ 'javascript' ]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v4
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v4
|
uses: github/codeql-action/autobuild@v3
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v4
|
uses: github/codeql-action/analyze@v3
|
||||||
|
|||||||
14
.github/workflows/codacy-analysis.yml
vendored
14
.github/workflows/codacy-analysis.yml
vendored
@@ -10,10 +10,10 @@ name: Codacy Security Scan
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ develop ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ develop ]
|
branches: [ master ]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '36 23 * * 3'
|
- cron: '36 23 * * 3'
|
||||||
|
|
||||||
@@ -22,21 +22,21 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
codacy-security-scan:
|
codacy-security-scan:
|
||||||
# Ensure schedule job never runs on forked repos. It's only executed for 'grokability/snipe-it'
|
# Ensure schedule job never runs on forked repos. It's only executed for 'snipe/snipe-it'
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # for actions/checkout to fetch code
|
contents: read # for actions/checkout to fetch code
|
||||||
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
||||||
if: (github.repository == 'grokability/snipe-it') || ((github.repository != 'grokability/snipe-it') && (github.event_name != 'schedule'))
|
if: (github.repository == 'snipe/snipe-it') || ((github.repository != 'snipe/snipe-it') && (github.event_name != 'schedule'))
|
||||||
name: Codacy Security Scan
|
name: Codacy Security Scan
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Checkout the repository to the GitHub Actions runner
|
# Checkout the repository to the GitHub Actions runner
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
|
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
|
||||||
- name: Run Codacy Analysis CLI
|
- name: Run Codacy Analysis CLI
|
||||||
uses: codacy/codacy-analysis-cli-action@v4.4.7
|
uses: codacy/codacy-analysis-cli-action@v4.4.5
|
||||||
with:
|
with:
|
||||||
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
|
# 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
|
# 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
|
# Upload the SARIF file generated in the previous step
|
||||||
- name: Upload SARIF results file
|
- name: Upload SARIF results file
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Crowdin push
|
- name: Crowdin push
|
||||||
uses: crowdin/github-action@v2
|
uses: crowdin/github-action@v2
|
||||||
|
|||||||
8
.github/workflows/docker-alpine.yml
vendored
8
.github/workflows/docker-alpine.yml
vendored
@@ -20,8 +20,8 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
|
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
|
||||||
if: github.repository == 'grokability/snipe-it'
|
if: github.repository == 'snipe/snipe-it'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
|
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
|
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
|
||||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
|
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
|
||||||
type=ref,event=tag,suffix=-alpine
|
type=ref,event=tag,suffix=-alpine
|
||||||
type=semver,pattern=v{{major}}-latest-alpine
|
type=semver,pattern=v{{major}}-latest-alpine
|
||||||
# Define default tag "flavor" for docker/metadata-action per
|
# Define default tag "flavor" for docker/metadata-action per
|
||||||
# https://github.com/docker/metadata-action#flavor-input
|
# https://github.com/docker/metadata-action#flavor-input
|
||||||
# We turn off 'latest' tag by default.
|
# We turn off 'latest' tag by default.
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# https://github.com/actions/checkout
|
# https://github.com/actions/checkout
|
||||||
- name: Checkout codebase
|
- name: Checkout codebase
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# https://github.com/docker/setup-buildx-action
|
# https://github.com/docker/setup-buildx-action
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Snipe-IT Docker image build for hub.docker.com
|
# Snipe-IT Docker image build for hub.docker.com
|
||||||
name: Docker images (Ubuntu)
|
name: Docker images
|
||||||
|
|
||||||
# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
|
# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
|
||||||
# Also run for PRs to ensure PR doesn't break Docker build process
|
# Also run for PRs to ensure PR doesn't break Docker build process
|
||||||
@@ -20,8 +20,8 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
|
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
|
||||||
if: github.repository == 'grokability/snipe-it'
|
if: github.repository == 'snipe/snipe-it'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
|
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
|
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
|
||||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
|
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
type=semver,pattern=v{{major}}-latest
|
type=semver,pattern=v{{major}}-latest
|
||||||
# Define default tag "flavor" for docker/metadata-action per
|
# Define default tag "flavor" for docker/metadata-action per
|
||||||
# https://github.com/docker/metadata-action#flavor-input
|
# https://github.com/docker/metadata-action#flavor-input
|
||||||
# We turn off 'latest' tag by default.
|
# We turn off 'latest' tag by default.
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# https://github.com/actions/checkout
|
# https://github.com/actions/checkout
|
||||||
- name: Checkout codebase
|
- name: Checkout codebase
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# https://github.com/docker/setup-buildx-action
|
# https://github.com/docker/setup-buildx-action
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
2
.github/workflows/dockerhub-description.yml
vendored
2
.github/workflows/dockerhub-description.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
dockerHubDescription:
|
dockerHubDescription:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker Hub Description
|
- name: Docker Hub Description
|
||||||
uses: grokability/dockerhub-description@7ea9d275c7cdbe2b676a093a0308c50665e3b8b4
|
uses: grokability/dockerhub-description@7ea9d275c7cdbe2b676a093a0308c50665e3b8b4
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
# pull-requests: write
|
# pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v10
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
debug-only: true
|
debug-only: true
|
||||||
ascending: 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 }}"
|
php-version: "${{ matrix.php-version }}"
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
- name: Get Composer Cache Directory
|
||||||
id: composer-cache
|
id: composer-cache
|
||||||
@@ -76,16 +76,4 @@ jobs:
|
|||||||
DB_DATABASE: snipeit
|
DB_DATABASE: snipeit
|
||||||
DB_PORT: ${{ job.services.mysql.ports[3306] }}
|
DB_PORT: ${{ job.services.mysql.ports[3306] }}
|
||||||
DB_USERNAME: root
|
DB_USERNAME: root
|
||||||
LOG_CHANNEL: single
|
|
||||||
LOG_LEVEL: debug
|
|
||||||
run: php artisan test
|
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 }}"
|
php-version: "${{ matrix.php-version }}"
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
- name: Get Composer Cache Directory
|
||||||
id: composer-cache
|
id: composer-cache
|
||||||
@@ -75,16 +75,4 @@ jobs:
|
|||||||
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
|
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
|
||||||
DB_USERNAME: snipeit
|
DB_USERNAME: snipeit
|
||||||
DB_PASSWORD: password
|
DB_PASSWORD: password
|
||||||
LOG_CHANNEL: single
|
|
||||||
LOG_LEVEL: debug
|
|
||||||
run: php artisan test
|
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 }}"
|
php-version: "${{ matrix.php-version }}"
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
- name: Get Composer Cache Directory
|
||||||
id: composer-cache
|
id: composer-cache
|
||||||
@@ -61,16 +61,4 @@ jobs:
|
|||||||
- name: Execute tests (Unit and Feature tests) via PHPUnit
|
- name: Execute tests (Unit and Feature tests) via PHPUnit
|
||||||
env:
|
env:
|
||||||
DB_CONNECTION: sqlite
|
DB_CONNECTION: sqlite
|
||||||
LOG_CHANNEL: single
|
|
||||||
LOG_LEVEL: debug
|
|
||||||
run: php artisan test
|
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
|
|
||||||
|
|||||||
240
.pa11yci.json
240
.pa11yci.json
@@ -1,240 +0,0 @@
|
|||||||
{
|
|
||||||
"standard": "WCAG2AA",
|
|
||||||
"level": "error",
|
|
||||||
"defaults": {
|
|
||||||
"useIncognitoBrowserContext": false,
|
|
||||||
"timeout": 500000,
|
|
||||||
"wait": 5000,
|
|
||||||
"ignore" : [
|
|
||||||
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G145.Fail",
|
|
||||||
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
|
|
||||||
],
|
|
||||||
|
|
||||||
"viewport": {
|
|
||||||
"width": 1280,
|
|
||||||
"height": 1024
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"urls": [
|
|
||||||
{
|
|
||||||
"__NOTE" : "this should always be FIRST (if browser context is preserved)",
|
|
||||||
"url": "https://snipe-it.test/login",
|
|
||||||
"actions": [
|
|
||||||
"navigate to https://snipe-it.test/login",
|
|
||||||
"screen capture tests/pa11y/login.png",
|
|
||||||
"set field input[name='username'] to admin",
|
|
||||||
"set field input[name='password'] to password",
|
|
||||||
"click element button[type=submit]",
|
|
||||||
"wait for url to be https://snipe-it.test/",
|
|
||||||
"screen capture tests/pa11y/dashboard.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/admin",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/admin",
|
|
||||||
"screen capture tests/pa11y/admin-settings.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/admin/branding",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/admin/branding",
|
|
||||||
"screen capture tests/pa11y/admin-branding.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/admin/general",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/admin/general",
|
|
||||||
"screen capture tests/pa11y/admin-general.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/hardware/create",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/hardware/create",
|
|
||||||
"screen capture tests/pa11y/asset-create.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/hardware",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/hardware",
|
|
||||||
"screen capture tests/pa11y/asset-list.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/hardware/1",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/hardware/1",
|
|
||||||
"screen capture tests/pa11y/asset-detail.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/account/view-assets",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/account/view-assets",
|
|
||||||
"screen capture tests/pa11y/profile.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/licences",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/licenses",
|
|
||||||
"screen capture tests/pa11y/license-list.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/licences/create",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/licenses/create",
|
|
||||||
"screen capture tests/pa11y/license-create.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/licences/1",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/licenses/1",
|
|
||||||
"screen capture tests/pa11y/license-view.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/consumables",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/consumables",
|
|
||||||
"screen capture tests/pa11y/consumable-list.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/consumables/create",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/consumables/create",
|
|
||||||
"screen capture tests/pa11y/consumable-create.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/consumables/1",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/consumables/1",
|
|
||||||
"screen capture tests/pa11y/consumable-view.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/accessories",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/accessories",
|
|
||||||
"screen capture tests/pa11y/accessory-list.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/accessories/create",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/accessories/create",
|
|
||||||
"screen capture tests/pa11y/accessory-create.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/accessories/1",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/accessories/1",
|
|
||||||
"screen capture tests/pa11y/accessory-view.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/locations",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/locations",
|
|
||||||
"screen capture tests/pa11y/location-list.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/locations/create",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/locations/create",
|
|
||||||
"screen capture tests/pa11y/location-create.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/locations/1",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/locations/1",
|
|
||||||
"screen capture tests/pa11y/location-view.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/models",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/models",
|
|
||||||
"screen capture tests/pa11y/model-list.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/models/create",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/models/create",
|
|
||||||
"screen capture tests/pa11y/model-create.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/models/1",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/models/1",
|
|
||||||
"screen capture tests/pa11y/model-view.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/companies",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/companies",
|
|
||||||
"screen capture tests/pa11y/company-list.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/companies/create",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/companies/create",
|
|
||||||
"screen capture tests/pa11y/company-create.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/companies/1",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/companies/1",
|
|
||||||
"screen capture tests/pa11y/company-view.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/departments",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/departments",
|
|
||||||
"screen capture tests/pa11y/department-list.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/departments/create",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/departments/create",
|
|
||||||
"screen capture tests/pa11y/department-create.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/departments/1",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/departments/1",
|
|
||||||
"screen capture tests/pa11y/department-view.png"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"url" : "https://snipe-it.test/invalid-url",
|
|
||||||
"actions" : [
|
|
||||||
"navigate to https://snipe-it.test/invalid-url",
|
|
||||||
"screen capture tests/pa11y/404.png"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -42,33 +42,18 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
|
|||||||
| [<img src="https://avatars.githubusercontent.com/u/1975640?v=4" width="110px;"/><br /><sub>Evan Taylor</sub>](https://github.com/Delta5)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Delta5 "Code") | [<img src="https://avatars.githubusercontent.com/u/8735148?v=4" width="110px;"/><br /><sub>Petri Asikainen</sub>](https://github.com/PetriAsi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PetriAsi "Code") | [<img src="https://avatars.githubusercontent.com/u/11424540?v=4" width="110px;"/><br /><sub>derdeagle</sub>](https://github.com/derdeagle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derdeagle "Code") | [<img src="https://avatars.githubusercontent.com/u/176950?v=4" width="110px;"/><br /><sub>Mike Frysinger</sub>](https://wh0rd.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vapier "Code") | [<img src="https://avatars.githubusercontent.com/u/22044358?v=4" width="110px;"/><br /><sub>ALPHA</sub>](https://github.com/AL4AL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AL4AL "Code") | [<img src="https://avatars.githubusercontent.com/u/1042587?v=4" width="110px;"/><br /><sub>FliegenKLATSCH</sub>](https://www.ifern.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH "Code") | [<img src="https://avatars.githubusercontent.com/u/442138?v=4" width="110px;"/><br /><sub>Jeremy Price</sub>](https://github.com/jerm)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jerm "Code") |
|
| [<img src="https://avatars.githubusercontent.com/u/1975640?v=4" width="110px;"/><br /><sub>Evan Taylor</sub>](https://github.com/Delta5)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Delta5 "Code") | [<img src="https://avatars.githubusercontent.com/u/8735148?v=4" width="110px;"/><br /><sub>Petri Asikainen</sub>](https://github.com/PetriAsi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PetriAsi "Code") | [<img src="https://avatars.githubusercontent.com/u/11424540?v=4" width="110px;"/><br /><sub>derdeagle</sub>](https://github.com/derdeagle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derdeagle "Code") | [<img src="https://avatars.githubusercontent.com/u/176950?v=4" width="110px;"/><br /><sub>Mike Frysinger</sub>](https://wh0rd.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vapier "Code") | [<img src="https://avatars.githubusercontent.com/u/22044358?v=4" width="110px;"/><br /><sub>ALPHA</sub>](https://github.com/AL4AL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AL4AL "Code") | [<img src="https://avatars.githubusercontent.com/u/1042587?v=4" width="110px;"/><br /><sub>FliegenKLATSCH</sub>](https://www.ifern.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH "Code") | [<img src="https://avatars.githubusercontent.com/u/442138?v=4" width="110px;"/><br /><sub>Jeremy Price</sub>](https://github.com/jerm)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jerm "Code") |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/84392209?v=4" width="110px;"/><br /><sub>Toreg87</sub>](https://github.com/Toreg87)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Toreg87 "Code") | [<img src="https://avatars.githubusercontent.com/u/67638596?v=4" width="110px;"/><br /><sub>Matthew Nickson</sub>](https://github.com/Computroniks)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Computroniks "Code") | [<img src="https://avatars.githubusercontent.com/u/1646397?v=4" width="110px;"/><br /><sub>Jethro Nederhof</sub>](https://jethron.id.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jethron "Code") | [<img src="https://avatars.githubusercontent.com/u/23289826?v=4" width="110px;"/><br /><sub>Oskar Stenberg</sub>](https://github.com/01ste02)<br />[💻](https://github.com/snipe/snipe-it/commits?author=01ste02 "Code") | [<img src="https://avatars.githubusercontent.com/u/82208283?v=4" width="110px;"/><br /><sub>Robert-Azelis</sub>](https://github.com/Robert-Azelis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Robert-Azelis "Code") | [<img src="https://avatars.githubusercontent.com/u/60648387?v=4" width="110px;"/><br /><sub>Alexander William Smith</sub>](https://github.com/alwism)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alwism "Code") | [<img src="https://avatars.githubusercontent.com/u/24418301?v=4" width="110px;"/><br /><sub>LEITWERK AG</sub>](https://www.leitwerk.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leitwerk-ag "Code") |
|
| [<img src="https://avatars.githubusercontent.com/u/84392209?v=4" width="110px;"/><br /><sub>Toreg87</sub>](https://github.com/Toreg87)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Toreg87 "Code") | [<img src="https://avatars.githubusercontent.com/u/67638596?v=4" width="110px;"/><br /><sub>Matthew Nickson</sub>](https://github.com/Computroniks)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Computroniks "Code") | [<img src="https://avatars.githubusercontent.com/u/1646397?v=4" width="110px;"/><br /><sub>Jethro Nederhof</sub>](https://jethron.id.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jethron "Code") | [<img src="https://avatars.githubusercontent.com/u/23289826?v=4" width="110px;"/><br /><sub>Oskar Stenberg</sub>](https://github.com/01ste02)<br />[💻](https://github.com/snipe/snipe-it/commits?author=01ste02 "Code") | [<img src="https://avatars.githubusercontent.com/u/82208283?v=4" width="110px;"/><br /><sub>Robert-Azelis</sub>](https://github.com/Robert-Azelis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Robert-Azelis "Code") | [<img src="https://avatars.githubusercontent.com/u/60648387?v=4" width="110px;"/><br /><sub>Alexander William Smith</sub>](https://github.com/alwism)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alwism "Code") | [<img src="https://avatars.githubusercontent.com/u/24418301?v=4" width="110px;"/><br /><sub>LEITWERK AG</sub>](https://www.leitwerk.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leitwerk-ag "Code") |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/1911435?v=4" width="110px;"/><br /><sub>Adam</sub>](http://www.aboutcher.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamboutcher "Code") | [<img src="https://avatars.githubusercontent.com/u/16104273?v=4" width="110px;"/><br /><sub>Ian</sub>](https://snksrv.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sneak-it "Code") | [<img src="https://avatars.githubusercontent.com/u/4023909?v=4" width="110px;"/><br /><sub>Shao Yu-Lung (Allen)</sub>](http://blog.bestlong.idv.tw/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bestlong "Code") | [<img src="https://avatars.githubusercontent.com/u/76475453?v=4" width="110px;"/><br /><sub>Haxatron</sub>](https://github.com/Haxatron)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Haxatron "Code") | [<img src="https://avatars.githubusercontent.com/u/88776392?v=4" width="110px;"/><br /><sub>PlaneNuts</sub>](https://github.com/PlaneNuts)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PlaneNuts "Code") | [<img src="https://avatars.githubusercontent.com/u/3842948?v=4" width="110px;"/><br /><sub>Bradley Coudriet</sub>](http://bjcpgd.cias.rit.edu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=exula "Code") | [<img src="https://avatars.githubusercontent.com/u/21966173?v=4" width="110px;"/><br /><sub>Dalton Durst</sub>](https://daltondur.st)<br />[💻](https://github.com/snipe/snipe-it/commits?author=UniversalSuperBox "Code") |
|
| [<img src="https://avatars.githubusercontent.com/u/1911435?v=4" width="110px;"/><br /><sub>Adam</sub>](http://www.aboutcher.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamboutcher "Code") | [<img src="https://avatars.githubusercontent.com/u/16104273?v=4" width="110px;"/><br /><sub>Ian</sub>](https://snksrv.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sneak-it "Code") | [<img src="https://avatars.githubusercontent.com/u/4023909?v=4" width="110px;"/><br /><sub>Shao Yu-Lung (Allen)</sub>](http://blog.bestlong.idv.tw/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bestlong "Code") | [<img src="https://avatars.githubusercontent.com/u/76475453?v=4" width="110px;"/><br /><sub>Haxatron</sub>](https://github.com/Haxatron)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Haxatron "Code") | [<img src="https://avatars.githubusercontent.com/u/88776392?v=4" width="110px;"/><br /><sub>PlaneNuts</sub>](https://github.com/PlaneNuts)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PlaneNuts "Code") | [<img src="https://avatars.githubusercontent.com/u/3842948?v=4" width="110px;"/><br /><sub>Bradley Coudriet</sub>](http://bjcpgd.cias.rit.edu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=exula "Code") | [<img src="https://avatars.githubusercontent.com/u/21966173?v=4" width="110px;"/><br /><sub>Dalton Durst</sub>](https://daltondur.st)<br />[💻](https://github.com/snipe/snipe-it/commits?author=UniversalSuperBox "Code") |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/38761237?v=4" width="110px;"/><br /><sub>Alex Janes</sub>](https://adagiohealth.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adagioajanes "Code") | [<img src="https://avatars.githubusercontent.com/u/32387849?v=4" width="110px;"/><br /><sub>Nuraeil</sub>](https://github.com/nuraeil)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nuraeil "Code") | [<img src="https://avatars.githubusercontent.com/u/48162670?v=4" width="110px;"/><br /><sub>TenOfTens</sub>](https://github.com/TenOfTens)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TenOfTens "Code") | [<img src="https://avatars.githubusercontent.com/u/9415391?v=4" width="110px;"/><br /><sub>waffle</sub>](https://ditisjens.be/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=insert-waffle "Code") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/qveensi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qveensi "Code") | [<img src="https://avatars.githubusercontent.com/u/3839381?v=4" width="110px;"/><br /><sub>Achmad Fienan Rahardianto</sub>](https://github.com/veenone)<br />[💻](https://github.com/snipe/snipe-it/commits?author=veenone "Code") | [<img src="https://avatars.githubusercontent.com/u/97299851?v=4" width="110px;"/><br /><sub>Christian Weirich</sub>](https://github.com/chrisweirich)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chrisweirich "Code") |
|
| [<img src="https://avatars.githubusercontent.com/u/38761237?v=4" width="110px;"/><br /><sub>Alex Janes</sub>](https://adagiohealth.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adagioajanes "Code") | [<img src="https://avatars.githubusercontent.com/u/32387849?v=4" width="110px;"/><br /><sub>Nuraeil</sub>](https://github.com/nuraeil)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nuraeil "Code") | [<img src="https://avatars.githubusercontent.com/u/48162670?v=4" width="110px;"/><br /><sub>TenOfTens</sub>](https://github.com/TenOfTens)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TenOfTens "Code") | [<img src="https://avatars.githubusercontent.com/u/9415391?v=4" width="110px;"/><br /><sub>waffle</sub>](https://ditisjens.be/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=insert-waffle "Code") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/QveenSi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=QveenSi "Code") | [<img src="https://avatars.githubusercontent.com/u/3839381?v=4" width="110px;"/><br /><sub>Achmad Fienan Rahardianto</sub>](https://github.com/veenone)<br />[💻](https://github.com/snipe/snipe-it/commits?author=veenone "Code") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/QveenSi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=QveenSi "Code") |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/1294403?v=4" width="110px;"/><br /><sub>denzfarid</sub>](https://github.com/denzfarid)<br /> | [<img src="https://avatars.githubusercontent.com/u/94018771?v=4" width="110px;"/><br /><sub>ntbutler-nbcs</sub>](https://github.com/ntbutler-nbcs)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs "Code") | [<img src="https://avatars.githubusercontent.com/u/172697?v=4" width="110px;"/><br /><sub>Naveen</sub>](https://naveensrinivasan.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=naveensrinivasan "Code") | [<img src="https://avatars.githubusercontent.com/u/55674383?v=4" width="110px;"/><br /><sub>Mike Roquemore</sub>](https://github.com/mikeroq)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikeroq "Code") | [<img src="https://avatars.githubusercontent.com/u/7991086?v=4" width="110px;"/><br /><sub>Daniel Reeder</sub>](https://github.com/reederda)<br />[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=reederda "Code") | [<img src="https://avatars.githubusercontent.com/u/109422491?v=4" width="110px;"/><br /><sub>vickyjaura183</sub>](https://github.com/vickyjaura183)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vickyjaura183 "Code") | [<img src="https://avatars.githubusercontent.com/u/32363424?v=4" width="110px;"/><br /><sub>Peace</sub>](https://github.com/julian-piehl)<br />[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") |
|
| [<img src="https://avatars.githubusercontent.com/u/97299851?v=4" width="110px;"/><br /><sub>Christian Weirich</sub>](https://github.com/chrisweirich)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chrisweirich "Code") | [<img src="https://avatars.githubusercontent.com/u/1294403?v=4" width="110px;"/><br /><sub>denzfarid</sub>](https://github.com/denzfarid)<br /> | [<img src="https://avatars.githubusercontent.com/u/94018771?v=4" width="110px;"/><br /><sub>ntbutler-nbcs</sub>](https://github.com/ntbutler-nbcs)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs "Code") | [<img src="https://avatars.githubusercontent.com/u/172697?v=4" width="110px;"/><br /><sub>Naveen</sub>](https://naveensrinivasan.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=naveensrinivasan "Code") | [<img src="https://avatars.githubusercontent.com/u/55674383?v=4" width="110px;"/><br /><sub>Mike Roquemore</sub>](https://github.com/mikeroq)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikeroq "Code") | [<img src="https://avatars.githubusercontent.com/u/7991086?v=4" width="110px;"/><br /><sub>Daniel Reeder</sub>](https://github.com/reederda)<br />[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=reederda "Code") | [<img src="https://avatars.githubusercontent.com/u/109422491?v=4" width="110px;"/><br /><sub>vickyjaura183</sub>](https://github.com/vickyjaura183)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vickyjaura183 "Code") |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/231528?v=4" width="110px;"/><br /><sub>Kyle Gordon</sub>](https://github.com/kylegordon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [<img src="https://avatars.githubusercontent.com/u/53009155?v=4" width="110px;"/><br /><sub>Katharina Drexel</sub>](http://www.bfh.ch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [<img src="https://avatars.githubusercontent.com/u/1931963?v=4" width="110px;"/><br /><sub>David Sferruzza</sub>](https://david.sferruzza.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [<img src="https://avatars.githubusercontent.com/u/19511639?v=4" width="110px;"/><br /><sub>Rick Nelson</sub>](https://github.com/rnelsonee)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [<img src="https://avatars.githubusercontent.com/u/94169344?v=4" width="110px;"/><br /><sub>BasO12</sub>](https://github.com/BasO12)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [<img src="https://avatars.githubusercontent.com/u/111710123?v=4" width="110px;"/><br /><sub>Vautia</sub>](https://github.com/Vautia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") | [<img src="https://avatars.githubusercontent.com/u/28321?v=4" width="110px;"/><br /><sub>Chris Hartjes</sub>](http://www.littlehart.net/atthekeyboard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") |
|
| [<img src="https://avatars.githubusercontent.com/u/32363424?v=4" width="110px;"/><br /><sub>Peace</sub>](https://github.com/julian-piehl)<br />[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") | [<img src="https://avatars.githubusercontent.com/u/231528?v=4" width="110px;"/><br /><sub>Kyle Gordon</sub>](https://github.com/kylegordon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [<img src="https://avatars.githubusercontent.com/u/53009155?v=4" width="110px;"/><br /><sub>Katharina Drexel</sub>](http://www.bfh.ch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [<img src="https://avatars.githubusercontent.com/u/1931963?v=4" width="110px;"/><br /><sub>David Sferruzza</sub>](https://david.sferruzza.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [<img src="https://avatars.githubusercontent.com/u/19511639?v=4" width="110px;"/><br /><sub>Rick Nelson</sub>](https://github.com/rnelsonee)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [<img src="https://avatars.githubusercontent.com/u/94169344?v=4" width="110px;"/><br /><sub>BasO12</sub>](https://github.com/BasO12)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [<img src="https://avatars.githubusercontent.com/u/111710123?v=4" width="110px;"/><br /><sub>Vautia</sub>](https://github.com/Vautia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/2404584?v=4" width="110px;"/><br /><sub>geo-chen</sub>](https://github.com/geo-chen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [<img src="https://avatars.githubusercontent.com/u/6006620?v=4" width="110px;"/><br /><sub>Phan Nguyen</sub>](https://github.com/nh314)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [<img src="https://avatars.githubusercontent.com/u/115993812?v=4" width="110px;"/><br /><sub>Iisakki Jaakkola</sub>](https://github.com/StarlessNights)<br />[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="110px;"/><br /><sub>Ikko Ashimine</sub>](https://bandism.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [<img src="https://avatars.githubusercontent.com/u/56871540?v=4" width="110px;"/><br /><sub>Lukas Fehling</sub>](https://github.com/lukasfehling)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [<img src="https://avatars.githubusercontent.com/u/1975990?v=4" width="110px;"/><br /><sub>Fernando Almeida</sub>](https://github.com/fernando-almeida)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") | [<img src="https://avatars.githubusercontent.com/u/116301219?v=4" width="110px;"/><br /><sub>akemidx</sub>](https://github.com/akemidx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") |
|
| [<img src="https://avatars.githubusercontent.com/u/28321?v=4" width="110px;"/><br /><sub>Chris Hartjes</sub>](http://www.littlehart.net/atthekeyboard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") | [<img src="https://avatars.githubusercontent.com/u/2404584?v=4" width="110px;"/><br /><sub>geo-chen</sub>](https://github.com/geo-chen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [<img src="https://avatars.githubusercontent.com/u/6006620?v=4" width="110px;"/><br /><sub>Phan Nguyen</sub>](https://github.com/nh314)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [<img src="https://avatars.githubusercontent.com/u/115993812?v=4" width="110px;"/><br /><sub>Iisakki Jaakkola</sub>](https://github.com/StarlessNights)<br />[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="110px;"/><br /><sub>Ikko Ashimine</sub>](https://bandism.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [<img src="https://avatars.githubusercontent.com/u/56871540?v=4" width="110px;"/><br /><sub>Lukas Fehling</sub>](https://github.com/lukasfehling)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [<img src="https://avatars.githubusercontent.com/u/1975990?v=4" width="110px;"/><br /><sub>Fernando Almeida</sub>](https://github.com/fernando-almeida)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/144778?v=4" width="110px;"/><br /><sub>Oguz Bilgic</sub>](http://oguz.site)<br />[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [<img src="https://avatars.githubusercontent.com/u/9262438?v=4" width="110px;"/><br /><sub>Scooter Crawford</sub>](https://github.com/scoo73r)<br />[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [<img src="https://avatars.githubusercontent.com/u/5957345?v=4" width="110px;"/><br /><sub>subdriven</sub>](https://github.com/subdriven)<br />[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [<img src="https://avatars.githubusercontent.com/u/658865?v=4" width="110px;"/><br /><sub>Andrew Savinykh</sub>](https://github.com/AndrewSav)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [<img src="https://avatars.githubusercontent.com/u/1155067?v=4" width="110px;"/><br /><sub>Tadayuki Onishi</sub>](https://kenchan0130.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [<img src="https://avatars.githubusercontent.com/u/112496896?v=4" width="110px;"/><br /><sub>Florian</sub>](https://github.com/floschoepfer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") | [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") |
|
| [<img src="https://avatars.githubusercontent.com/u/116301219?v=4" width="110px;"/><br /><sub>akemidx</sub>](https://github.com/akemidx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") | [<img src="https://avatars.githubusercontent.com/u/144778?v=4" width="110px;"/><br /><sub>Oguz Bilgic</sub>](http://oguz.site)<br />[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [<img src="https://avatars.githubusercontent.com/u/9262438?v=4" width="110px;"/><br /><sub>Scooter Crawford</sub>](https://github.com/scoo73r)<br />[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [<img src="https://avatars.githubusercontent.com/u/5957345?v=4" width="110px;"/><br /><sub>subdriven</sub>](https://github.com/subdriven)<br />[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [<img src="https://avatars.githubusercontent.com/u/658865?v=4" width="110px;"/><br /><sub>Andrew Savinykh</sub>](https://github.com/AndrewSav)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [<img src="https://avatars.githubusercontent.com/u/1155067?v=4" width="110px;"/><br /><sub>Tadayuki Onishi</sub>](https://kenchan0130.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [<img src="https://avatars.githubusercontent.com/u/112496896?v=4" width="110px;"/><br /><sub>Florian</sub>](https://github.com/floschoepfer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> | [<img src="https://avatars.githubusercontent.com/u/5731963?v=4" width="110px;"/><br /><sub>dboth</sub>](http://dboth.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [<img src="https://avatars.githubusercontent.com/u/87536651?v=4" width="110px;"/><br /><sub>Zachary Fleck</sub>](https://github.com/zacharyfleck)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | [<img src="https://avatars.githubusercontent.com/u/74609912?v=4" width="110px;"/><br /><sub>VIKAAS-A</sub>](https://github.com/vikaas-cyper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vikaas-cyper "Code") | [<img src="https://avatars.githubusercontent.com/u/88882041?v=4" width="110px;"/><br /><sub>Abdul Kareem</sub>](https://github.com/ak-piracha)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ak-piracha "Code") | [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") |
|
| [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") | [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> | [<img src="https://avatars.githubusercontent.com/u/5731963?v=4" width="110px;"/><br /><sub>dboth</sub>](http://dboth.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [<img src="https://avatars.githubusercontent.com/u/87536651?v=4" width="110px;"/><br /><sub>Zachary Fleck</sub>](https://github.com/zacharyfleck)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | [<img src="https://avatars.githubusercontent.com/u/74609912?v=4" width="110px;"/><br /><sub>VIKAAS-A</sub>](https://github.com/vikaas-cyper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vikaas-cyper "Code") | [<img src="https://avatars.githubusercontent.com/u/88882041?v=4" width="110px;"/><br /><sub>Abdul Kareem</sub>](https://github.com/ak-piracha)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ak-piracha "Code") |
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") | [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") |
|
| [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") | [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") |
|
||||||
| [<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/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") | [<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/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/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/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/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/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/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/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/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/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") |
|
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/3302372?v=4" width="110px;"/><br /><sub>Jared Busch</sub>](https://github.com/sorvani)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sorvani "Code") | [<img src="https://avatars.githubusercontent.com/u/111956991?v=4" width="110px;"/><br /><sub>seanborg-codethink</sub>](https://github.com/seanborg-codethink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanborg-codethink "Code") | [<img src="https://avatars.githubusercontent.com/u/160669961?v=4" width="110px;"/><br /><sub>dkaatz</sub>](https://github.com/dkaatz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dkaatz "Code") | [<img src="https://avatars.githubusercontent.com/u/827205?v=4" width="110px;"/><br /><sub>Daniel Ruf</sub>](https://threema.id/74SF7MW6?text=)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DanielRuf "Code") | [<img src="https://avatars.githubusercontent.com/u/38883201?v=4" width="110px;"/><br /><sub>ahpaleus</sub>](https://github.com/ahpaleus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ahpaleus "Code") | [<img src="https://avatars.githubusercontent.com/u/22906055?v=4" width="110px;"/><br /><sub>Anh DAO-DUY</sub>](https://github.com/mink-adao-duy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mink-adao-duy "Code") | [<img src="https://avatars.githubusercontent.com/u/4723453?v=4" width="110px;"/><br /><sub>Andres Gutierrez</sub>](https://github.com/Serdnad)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Serdnad "Code") |
|
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/111083379?v=4" width="110px;"/><br /><sub>Warren White</sub>](https://github.com/wewhite)<br />[💻](https://github.com/snipe/snipe-it/commits?author=wewhite "Code") | [<img src="https://avatars.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://robintemme.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=robintemme "Code") | [<img src="https://avatars.githubusercontent.com/u/47008367?v=4" width="110px;"/><br /><sub>herroworrd</sub>](https://github.com/herroworrd)<br />[💻](https://github.com/snipe/snipe-it/commits?author=herroworrd "Code") | [<img src="https://avatars.githubusercontent.com/u/28558609?v=4" width="110px;"/><br /><sub>vicleos</sub>](https://mubiu.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vicleos "Code") | [<img src="https://avatars.githubusercontent.com/u/1016780?v=4" width="110px;"/><br /><sub>Bob Clough</sub>](http://thinkl33t.co.uk/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thinkl33t "Code") | [<img src="https://avatars.githubusercontent.com/u/10648463?v=4" width="110px;"/><br /><sub>Brandon Daniel Bailey</sub>](https://github.com/brandon-bailey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brandon-bailey "Code") | [<img src="https://avatars.githubusercontent.com/u/23556080?v=4" width="110px;"/><br /><sub>Marc Bartelt</sub>](https://github.com/marcquark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcquark "Code") |
|
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/18286893?v=4" width="110px;"/><br /><sub>manu-crealytics</sub>](https://github.com/manu-crealytics)<br />[💻](https://github.com/snipe/snipe-it/commits?author=manu-crealytics "Code") | [<img src="https://avatars.githubusercontent.com/u/18245993?v=4" width="110px;"/><br /><sub>Konstantin Köhring</sub>](https://www.galaxy102.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Galaxy102 "Code") | [<img src="https://avatars.githubusercontent.com/u/685167?v=4" width="110px;"/><br /><sub>Deloz</sub>](https://deloz.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=deloz "Code") | [<img src="https://avatars.githubusercontent.com/u/2682426?v=4" width="110px;"/><br /><sub>Martin Berg</sub>](https://github.com/mbrrg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mbrrg "Code") | [<img src="https://avatars.githubusercontent.com/u/3694534?v=4" width="110px;"/><br /><sub>Richard Schwab</sub>](https://github.com/Nothing4You)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Nothing4You "Code") | [<img src="https://avatars.githubusercontent.com/u/8959676?v=4" width="110px;"/><br /><sub>Rick Heil</sub>](https://rickheil.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rickheil "Code") | [<img src="https://avatars.githubusercontent.com/u/397106?v=4" width="110px;"/><br /><sub>Ross Crawford-d'Heureuse</sub>](https://github.com/rosscdh)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rosscdh "Code") |
|
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/1621107?v=4" width="110px;"/><br /><sub>Ryan McGuire</sub>](https://github.com/McG800)<br />[💻](https://github.com/snipe/snipe-it/commits?author=McG800 "Code") | [<img src="https://avatars.githubusercontent.com/u/77835667?v=4" width="110px;"/><br /><sub>SBrown2021</sub>](https://github.com/SBrown2021)<br />[💻](https://github.com/snipe/snipe-it/commits?author=SBrown2021 "Code") | [<img src="https://avatars.githubusercontent.com/u/8780913?v=4" width="110px;"/><br /><sub>Serkan</sub>](https://github.com/serkanerip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=serkanerip "Code") | [<img src="https://avatars.githubusercontent.com/u/63188620?v=4" width="110px;"/><br /><sub>Shanks</sub>](https://www.yudelei.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Shankschn "Code") | [<img src="https://avatars.githubusercontent.com/u/198525698?v=4" width="110px;"/><br /><sub>cendai-mis</sub>](https://github.com/cendai-mis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cendai-mis "Code") | [<img src="https://avatars.githubusercontent.com/u/8724583?v=4" width="110px;"/><br /><sub>Shaun McPeck</sub>](https://smcpeck.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smcpeck "Code") | [<img src="https://avatars.githubusercontent.com/u/1378836?v=4" width="110px;"/><br /><sub>Stephen</sub>](https://github.com/snazy2000)<br />[💻](https://github.com/snipe/snipe-it/commits?author=snazy2000 "Code") |
|
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/4462739?v=4" width="110px;"/><br /><sub>Steven</sub>](http://nevets82.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Nevets82 "Code") | [<img src="https://avatars.githubusercontent.com/u/29017267?v=4" width="110px;"/><br /><sub>Mateus Villar</sub>](https://mateusvillar.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Mateus-Romera "Code") | [<img src="https://avatars.githubusercontent.com/u/12749393?v=4" width="110px;"/><br /><sub>Matthew Zackschewski</sub>](https://github.com/mzack5020)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mzack5020 "Code") | [<img src="https://avatars.githubusercontent.com/u/12660103?v=4" width="110px;"/><br /><sub>Matthias Frei</sub>](https://www.frei.media/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=firefrei "Code") | [<img src="https://avatars.githubusercontent.com/u/824840?v=4" width="110px;"/><br /><sub>Nenad Ticaric</sub>](https://github.com/nticaric)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nticaric "Code") | [<img src="https://avatars.githubusercontent.com/u/706439?v=4" width="110px;"/><br /><sub>Nikolay Didenko</sub>](https://github.com/Scorcher)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scorcher "Code") | [<img src="https://avatars.githubusercontent.com/u/5457236?v=4" width="110px;"/><br /><sub>Nuno Maduro</sub>](https://nunomaduro.com/sponsorships)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nunomaduro "Code") |
|
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/8883074?v=4" width="110px;"/><br /><sub>Oliver Walerys</sub>](https://tektikhq.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=owalerys "Code") | [<img src="https://avatars.githubusercontent.com/u/3102039?v=4" width="110px;"/><br /><sub>R. Christian McDonald</sub>](https://keybase.io/rcmcdonald91)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rcmcdonald91 "Code") | [<img src="https://avatars.githubusercontent.com/u/1525581?v=4" width="110px;"/><br /><sub>nix</sub>](https://nnix.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nixn "Code") | [<img src="https://avatars.githubusercontent.com/u/55462380?v=4" width="110px;"/><br /><sub>octobunny</sub>](https://github.com/octobunny)<br />[💻](https://github.com/snipe/snipe-it/commits?author=octobunny "Code") | [<img src="https://avatars.githubusercontent.com/u/8558670?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/sreyemnayr)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sreyemnayr "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>p3nj</sub>](https://benji.ltd/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=p3nj "Code") | [<img src="https://avatars.githubusercontent.com/u/6201617?v=4" width="110px;"/><br /><sub>Tim White</sub>](https://github.com/timwsuqld)<br />[💻](https://github.com/snipe/snipe-it/commits?author=timwsuqld "Code") |
|
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>yannikp</sub>](https://github.com/yannikp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=yannikp "Code") | [<img src="https://avatars.githubusercontent.com/u/20525448?v=4" width="110px;"/><br /><sub>victoria</sub>](https://github.com/viclou)<br />[💻](https://github.com/snipe/snipe-it/commits?author=viclou "Code") | [<img src="https://avatars.githubusercontent.com/u/40685314?v=4" width="110px;"/><br /><sub>Valentyn Tulub</sub>](https://github.com/valentyntu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=valentyntu "Code") | [<img src="https://avatars.githubusercontent.com/u/864520?v=4" width="110px;"/><br /><sub>Wouter van Os</sub>](http://wouter0100.nl/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Wouter0100 "Code") | [<img src="https://avatars.githubusercontent.com/u/3946540?v=4" width="110px;"/><br /><sub>Wyatt Teeter</sub>](https://www.linkedin.com/in/wyatt-teeter)<br />[💻](https://github.com/snipe/snipe-it/commits?author=xWyatt "Code") | [<img src="https://avatars.githubusercontent.com/u/1596124?v=4" width="110px;"/><br /><sub>Yorick Terweijden</sub>](https://github.com/terwey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=terwey "Code") | [<img src="https://avatars.githubusercontent.com/u/69298836?v=4" width="110px;"/><br /><sub>bmkalle</sub>](https://github.com/bmkalle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bmkalle "Code") |
|
|
||||||
| [<img src="https://avatars.githubusercontent.com/u/28403467?v=4" width="110px;"/><br /><sub>bricelabelle</sub>](https://github.com/bricelabelle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bricelabelle "Code") | [<img src="https://avatars.githubusercontent.com/u/97770090?v=4" width="110px;"/><br /><sub>corydlamb</sub>](https://github.com/corydlamb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=corydlamb "Code") | [<img src="https://avatars.githubusercontent.com/u/1154133?v=4" width="110px;"/><br /><sub>Diogenes S. Jesus</sub>](http://twitter.com/splash)<br />[💻](https://github.com/snipe/snipe-it/commits?author=splashx "Code") | [<img src="https://avatars.githubusercontent.com/u/5826629?v=4" width="110px;"/><br /><sub>D M</sub>](https://github.com/dkmansion)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dkmansion "Code") | [<img src="https://avatars.githubusercontent.com/u/14837699?v=4" width="110px;"/><br /><sub>Dustin B</sub>](https://github.com/Jarli01)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Jarli01 "Code") | [<img src="https://avatars.githubusercontent.com/u/348344?v=4" width="110px;"/><br /><sub>Fabian Grutschus</sub>](https://github.com/fabiang)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fabiang "Code") | [<img src="https://avatars.githubusercontent.com/u/1491053?v=4" width="110px;"/><br /><sub>MelonSmasher</sub>](https://github.com/MelonSmasher)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MelonSmasher "Code") |
|
|
||||||
| [<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/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 -->
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
|
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ FROM ubuntu:24.04
|
|||||||
LABEL maintainer="Brady Wetherington <bwetherington@grokability.com>"
|
LABEL maintainer="Brady Wetherington <bwetherington@grokability.com>"
|
||||||
|
|
||||||
# No need to add `apt-get clean` here, reference:
|
# No need to add `apt-get clean` here, reference:
|
||||||
# - https://github.com/grokability/snipe-it/pull/9201
|
# - https://github.com/snipe/snipe-it/pull/9201
|
||||||
# - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#apt-get
|
# - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#apt-get
|
||||||
|
|
||||||
RUN export DEBIAN_FRONTEND=noninteractive; \
|
RUN export DEBIAN_FRONTEND=noninteractive; \
|
||||||
@@ -110,7 +110,7 @@ COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
|||||||
|
|
||||||
# Get dependencies
|
# Get dependencies
|
||||||
USER docker
|
USER docker
|
||||||
RUN COMPOSER_CACHE_DIR=/dev/null composer install --no-dev --working-dir=/var/www/html && rm -rf /var/www/html/vendor/*/*/.git
|
RUN composer install --no-dev --working-dir=/var/www/html
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
############### APPLICATION INSTALL/INIT #################
|
############### APPLICATION INSTALL/INIT #################
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ COPY --from=composer /usr/bin/composer /usr/local/bin
|
|||||||
ARG COMPOSER_ALLOW_SUPERUSER=1
|
ARG COMPOSER_ALLOW_SUPERUSER=1
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
# Download and extract snipeit tarball
|
# Download and extract snipeit tarball
|
||||||
curl -o snipeit.tar.gz -fL "https://github.com/grokability/snipe-it/archive/v$SNIPEIT_RELEASE.tar.gz"; \
|
curl -o snipeit.tar.gz -fL "https://github.com/snipe/snipe-it/archive/v$SNIPEIT_RELEASE.tar.gz"; \
|
||||||
tar -xzf snipeit.tar.gz --strip-components=1 -C /var/www/html/; \
|
tar -xzf snipeit.tar.gz --strip-components=1 -C /var/www/html/; \
|
||||||
rm snipeit.tar.gz; \
|
rm snipeit.tar.gz; \
|
||||||
# Install composer php dependencies
|
# Install composer php dependencies
|
||||||
|
|||||||
37
README.md
37
README.md
@@ -1,6 +1,6 @@
|
|||||||

|

|
||||||
|
|
||||||
[](https://crowdin.com/project/snipe-it) [](https://hub.docker.com/r/snipe/snipe-it/) [](https://app.codacy.com/gh/grokability/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://github.com/grokability/snipe-it/actions/workflows/tests.yml)
|
[](https://crowdin.com/project/snipe-it) [](https://hub.docker.com/r/snipe/snipe-it/) [](https://app.codacy.com/gh/snipe/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [](https://github.com/snipe/snipe-it/actions/workflows/tests.yml)
|
||||||
[](#contributing) [](https://discord.gg/yZFtShAcKk)
|
[](#contributing) [](https://discord.gg/yZFtShAcKk)
|
||||||
|
|
||||||
## Snipe-IT - Open Source Asset Management System
|
## Snipe-IT - Open Source Asset Management System
|
||||||
@@ -9,7 +9,7 @@ This is a FOSS project for asset management in IT Operations. Knowing who has wh
|
|||||||
|
|
||||||
It is built on [Laravel 11](http://laravel.com).
|
It is built on [Laravel 11](http://laravel.com).
|
||||||
|
|
||||||
Snipe-IT is actively developed and we [release quite frequently](https://github.com/grokability/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).)
|
Snipe-IT is actively developed and we [release quite frequently](https://github.com/snipe/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).)
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> __This is web-based software__. This means there is no executable file (aka no .exe files), and it must be run on a web server and accessed through a web browser. It runs on any Mac OSX, any flavor of Linux, as well as Windows, and we have a [Docker image](https://snipe-it.readme.io/docs/docker) available if that's what you're into.
|
> __This is web-based software__. This means there is no executable file (aka no .exe files), and it must be run on a web server and accessed through a web browser. It runs on any Mac OSX, any flavor of Linux, as well as Windows, and we have a [Docker image](https://snipe-it.readme.io/docs/docker) available if that's what you're into.
|
||||||
@@ -44,7 +44,7 @@ For help using Snipe-IT, check out the [user's manual](https://snipe-it.readme.i
|
|||||||
-----
|
-----
|
||||||
### Bug Reports & Feature Requests
|
### Bug Reports & Feature Requests
|
||||||
|
|
||||||
Feel free to check out the [GitHub Issues for this project](https://github.com/grokability/snipe-it/issues) to open a bug report or see what open issues you can help with. Please search through existing issues (open *and* closed) to see if your question has already been answered before opening a new issue.
|
Feel free to check out the [GitHub Issues for this project](https://github.com/snipe/snipe-it/issues) to open a bug report or see what open issues you can help with. Please search through existing issues (open *and* closed) to see if your question has already been answered before opening a new issue.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> **PLEASE see the [Getting Help Guidelines](https://snipe-it.readme.io/docs/getting-help) and [Common Issues](https://snipe-it.readme.io/docs/common-issues) before opening a ticket, and be sure to complete all of the questions in the Github Issue template to help us to help you as quickly as possible.**
|
> **PLEASE see the [Getting Help Guidelines](https://snipe-it.readme.io/docs/getting-help) and [Common Issues](https://snipe-it.readme.io/docs/common-issues) before opening a ticket, and be sure to complete all of the questions in the Github Issue template to help us to help you as quickly as possible.**
|
||||||
@@ -76,37 +76,26 @@ Since the release of the JSON REST API, several third-party developers have been
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> As these were created by third-parties, Snipe-IT cannot provide support for these project, and you should contact the developers directly if you need assistance. Additionally, Snipe-IT makes no guarantees as to the reliability, accuracy or maintainability of these libraries. Use at your own risk. :)
|
> As these were created by third-parties, Snipe-IT cannot provide support for these project, and you should contact the developers directly if you need assistance. Additionally, Snipe-IT makes no guarantees as to the reliability, accuracy or maintainability of these libraries. Use at your own risk. :)
|
||||||
|
|
||||||
#### Libraries & Modules
|
- [Python Module](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
|
||||||
|
|
||||||
- [SnipeSharp - .NET module in C#](https://github.com/barrycarey/SnipeSharp) by [@barrycarey](https://github.com/barrycarey)
|
- [SnipeSharp - .NET module in C#](https://github.com/barrycarey/SnipeSharp) by [@barrycarey](https://github.com/barrycarey)
|
||||||
|
- [InQRy -unmaintained-](https://github.com/Microsoft/InQRy) by [@Microsoft](https://github.com/Microsoft)
|
||||||
- [SnipeitPS](https://github.com/snazy2000/SnipeitPS) by [@snazy2000](https://github.com/snazy2000) - Powershell API Wrapper for Snipe-it
|
- [SnipeitPS](https://github.com/snazy2000/SnipeitPS) by [@snazy2000](https://github.com/snazy2000) - Powershell API Wrapper for Snipe-it
|
||||||
- [jamf2snipe](https://github.com/grokability/jamf2snipe) - Python script to sync assets between a JAMFPro instance and a Snipe-IT instance
|
- [jamf2snipe](https://github.com/grokability/jamf2snipe) - Python script to sync assets between a JAMFPro instance and a Snipe-IT instance
|
||||||
- [jamf-snipe-rename](https://macblog.org/jamf-snipe-rename/) - Python script to rename computers in Jamf from Snipe-IT
|
- [jamf-snipe-rename](https://macblog.org/jamf-snipe-rename/) - Python script to rename computers in Jamf from Snipe-IT
|
||||||
|
- [Marksman](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
|
||||||
- [Snipe-IT plugin for Jira Service Desk](https://marketplace.atlassian.com/apps/1220964/snipe-it-for-jira)
|
- [Snipe-IT plugin for Jira Service Desk](https://marketplace.atlassian.com/apps/1220964/snipe-it-for-jira)
|
||||||
- [Python 3 CSV importer](https://github.com/gastamper/snipeit-csvimporter) - allows importing assets into Snipe-IT based on Item Name rather than Asset Tag.
|
- [Python 3 CSV importer](https://github.com/gastamper/snipeit-csvimporter) - allows importing assets into Snipe-IT based on Item Name rather than Asset Tag.
|
||||||
- [Snipe-IT Kubernetes Helm Chart](https://github.com/t3n/helm-charts/tree/master/snipeit) - For more information, [click here](https://hub.helm.sh/charts/t3n/snipeit).
|
- [Snipe-IT Kubernetes Helm Chart](https://github.com/t3n/helm-charts/tree/master/snipeit) - For more information, [click here](https://hub.helm.sh/charts/t3n/snipeit).
|
||||||
- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-IT.
|
- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-IT.
|
||||||
- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT.
|
- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT.
|
||||||
- [WWW::SnipeIT](https://github.com/SEDC/perl-www-snipeit) by [@SEDC](https://github.com/SEDC) - perl module for accessing the API
|
- [WWW::SnipeIT](https://github.com/SEDC/perl-www-snipeit) by [@SEDC](https://github.com/SEDC) - perl module for accessing the API
|
||||||
- [UniFi to Snipe-IT](https://www.edtechirl.com/p/snipe-it-and-azure-asset-management) originally by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT.
|
- [UniFi to Snipe-IT](https://github.com/RodneyLeeBrands/UnifiSnipeSync) by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT.
|
||||||
- [Kandji2Snipe](https://github.com/grokability/kandji2snipe) by [@briangoldstein](https://github.com/briangoldstein) - Python script that synchronizes Kandji with Snipe-IT.
|
- [Kandji2Snipe](https://github.com/grokability/kandji2snipe) by [@briangoldstein](https://github.com/briangoldstein) - Python script that synchronizes Kandji with Snipe-IT.
|
||||||
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by [@ReticentRobot](https://github.com/ReticentRobot) - Windows agent for Snipe-IT.
|
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by [@ReticentRobot](https://github.com/ReticentRobot) - Windows agent for Snipe-IT.
|
||||||
- [Gate Pass Generator](https://github.com/cha7uraAE/snipe-it-gate-pass-system) by [@cha7uraAE](https://github.com/cha7uraAE) - A Streamlit application for generating gate passes based on hardware data from a Snipe-IT API.
|
- [Gate Pass Generator](https://github.com/cha7uraAE/snipe-it-gate-pass-system) by [@cha7uraAE](https://github.com/cha7uraAE) - A Streamlit application for generating gate passes based on hardware data from a Snipe-IT API.
|
||||||
- [InQRy (archived)](https://github.com/Microsoft/InQRy) by [@Microsoft](https://github.com/Microsoft)
|
|
||||||
- [Marksman (archived)](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
|
|
||||||
- [Python Module (archived)](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
|
|
||||||
|
|
||||||
We also have a handful of [Google Apps scripts](https://github.com/grokability/google-apps-scripts-for-snipe-it) to help with various tasks.
|
We also have a handful of [Google Apps scripts](https://github.com/grokability/google-apps-scripts-for-snipe-it) to help with various tasks.
|
||||||
|
|
||||||
#### Mobile Apps
|
|
||||||
|
|
||||||
We're currently working on our own mobile app, but in the meantime, check out these third-party apps that work with Snipe-IT:
|
|
||||||
|
|
||||||
- [SnipeMate](https://snipemate.app/) (iOS, Google Play, Huawei AppGallery) by Mars Technology
|
|
||||||
- [Snipe-Scan](https://apps.apple.com/do/app/snipe-scan/id6744179400?uo=2) (iOS) by Nicolas Maton
|
|
||||||
- [Snipe-IT Assets Management](https://play.google.com/store/apps/details?id=com.diegogarciadev.assetsmanager.snipeit&hl=en&pli=1) (Google Play) by DiegoGarciaDEV
|
|
||||||
- [AssetX](https://apps.apple.com/my/app/assetx-for-snipe-it/id6741996196?uo=2) (iOS) for Snipe-IT by Rishi Gupta
|
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
### Join the Community!
|
### Join the Community!
|
||||||
@@ -133,15 +122,9 @@ The ERD is available [online here](https://drawsql.app/templates/snipe-it).
|
|||||||
|
|
||||||
Be sure to check out all of the [amazing people](CONTRIBUTORS.md) that have contributed to Snipe-IT over the years!
|
Be sure to check out all of the [amazing people](CONTRIBUTORS.md) that have contributed to Snipe-IT over the years!
|
||||||
|
|
||||||
-----
|
|
||||||
|
|
||||||
### Star History
|
|
||||||
|
|
||||||
[](https://www.star-history.com/#grokability/snipe-it&Date)
|
|
||||||
|
|
||||||
------
|
------
|
||||||
### Announcement List
|
### Announcement List
|
||||||
|
|
||||||
To be notified of important news (such as new releases, security advisories, etc), [sign up for our list](http://eepurl.com/XyZKz). We'll never sell or give away your info, and we'll only email you when it's important.
|
To be notified of important news (such as new releases, security advisories, etc), [sign up for our list](http://eepurl.com/XyZKz). We'll never sell or give away your info, and we'll only email you when it's important.
|
||||||
|
|
||||||
|
|
||||||
We also usually make smaller announcements on our social accounts, our Discord, and our blog, so be sure to subscribe to those if you're looking for more granular announcements.
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ make it impossible to backport security fixes on older versions.
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
|---------| ------------------ |
|
|---------| ------------------ |
|
||||||
| 8.x | :white_check_mark: |
|
|
||||||
| 7.x | :white_check_mark: |
|
| 7.x | :white_check_mark: |
|
||||||
| 6.x | :x: |
|
| 6.x | :x: |
|
||||||
| 5.1.x | :x: |
|
| 5.1.x | :x: |
|
||||||
|
|||||||
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@@ -1,7 +1,7 @@
|
|||||||
# -*- mode: ruby -*-
|
# -*- mode: ruby -*-
|
||||||
# vi: set ft=ruby :
|
# vi: set ft=ruby :
|
||||||
|
|
||||||
SNIPEIT_SH_URL= "https://raw.githubusercontent.com/grokability/snipe-it/master/snipeit.sh"
|
SNIPEIT_SH_URL= "https://raw.githubusercontent.com/snipe/snipe-it/master/snipeit.sh"
|
||||||
NETWORK_BRIDGE= "en0: Wi-Fi (AirPort)"
|
NETWORK_BRIDGE= "en0: Wi-Fi (AirPort)"
|
||||||
|
|
||||||
Vagrant.configure("2") do |config|
|
Vagrant.configure("2") do |config|
|
||||||
|
|||||||
2
app.json
2
app.json
@@ -6,7 +6,7 @@
|
|||||||
"it asset"
|
"it asset"
|
||||||
],
|
],
|
||||||
"website": "https://snipeitapp.com/",
|
"website": "https://snipeitapp.com/",
|
||||||
"repository": "https://github.com/grokability/snipe-it",
|
"repository": "https://github.com/snipe/snipe-it",
|
||||||
"logo": "https://pbs.twimg.com/profile_images/976748875733020672/K-HnZCCK_400x400.jpg",
|
"logo": "https://pbs.twimg.com/profile_images/976748875733020672/K-HnZCCK_400x400.jpg",
|
||||||
"success_url": "/setup",
|
"success_url": "/setup",
|
||||||
"env": {
|
"env": {
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions\CheckoutRequests;
|
|
||||||
|
|
||||||
use App\Models\Actionlog;
|
|
||||||
use App\Models\Asset;
|
|
||||||
use App\Models\Company;
|
|
||||||
use App\Models\Setting;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Notifications\RequestAssetCancelation;
|
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
|
||||||
|
|
||||||
class CancelCheckoutRequestAction
|
|
||||||
{
|
|
||||||
public static function run(Asset $asset, User $user)
|
|
||||||
{
|
|
||||||
if (!Company::isCurrentUserHasAccess($asset)) {
|
|
||||||
throw new AuthorizationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
$asset->cancelRequest();
|
|
||||||
|
|
||||||
$asset->decrement('requests_counter', 1);
|
|
||||||
|
|
||||||
$data['item'] = $asset;
|
|
||||||
$data['target'] = $user;
|
|
||||||
$data['item_quantity'] = 1;
|
|
||||||
$settings = Setting::getSettings();
|
|
||||||
|
|
||||||
$logaction = new Actionlog();
|
|
||||||
$logaction->item_id = $data['asset_id'] = $asset->id;
|
|
||||||
$logaction->item_type = $data['item_type'] = Asset::class;
|
|
||||||
$logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s');
|
|
||||||
$logaction->target_id = $data['user_id'] = auth()->id();
|
|
||||||
$logaction->target_type = User::class;
|
|
||||||
$logaction->location_id = $user->location_id ?? null;
|
|
||||||
$logaction->logaction('request canceled');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$settings->notify(new RequestAssetCancelation($data));
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
\Log::warning($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Actions\CheckoutRequests;
|
|
||||||
|
|
||||||
use App\Exceptions\AssetNotRequestable;
|
|
||||||
use App\Models\Actionlog;
|
|
||||||
use App\Models\Asset;
|
|
||||||
use App\Models\Company;
|
|
||||||
use App\Models\Setting;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Notifications\RequestAssetNotification;
|
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
|
||||||
use Log;
|
|
||||||
|
|
||||||
class CreateCheckoutRequestAction
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @throws AssetNotRequestable
|
|
||||||
* @throws AuthorizationException
|
|
||||||
*/
|
|
||||||
public static function run(Asset $asset, User $user): string
|
|
||||||
{
|
|
||||||
if (is_null(Asset::RequestableAssets()->find($asset->id))) {
|
|
||||||
throw new AssetNotRequestable($asset);
|
|
||||||
}
|
|
||||||
if (!Company::isCurrentUserHasAccess($asset)) {
|
|
||||||
throw new AuthorizationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
$data['item'] = $asset;
|
|
||||||
$data['target'] = $user;
|
|
||||||
$data['item_quantity'] = 1;
|
|
||||||
$settings = Setting::getSettings();
|
|
||||||
|
|
||||||
$logaction = new Actionlog();
|
|
||||||
$logaction->item_id = $data['asset_id'] = $asset->id;
|
|
||||||
$logaction->item_type = $data['item_type'] = Asset::class;
|
|
||||||
$logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s');
|
|
||||||
$logaction->target_id = $data['user_id'] = auth()->id();
|
|
||||||
$logaction->target_type = User::class;
|
|
||||||
$logaction->location_id = $user->location_id ?? null;
|
|
||||||
$logaction->logaction('requested');
|
|
||||||
|
|
||||||
$asset->request();
|
|
||||||
$asset->increment('requests_counter', 1);
|
|
||||||
try {
|
|
||||||
$settings->notify(new RequestAssetNotification($data));
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Log::warning($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<?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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<?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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class DisableSAML extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'snipeit:saml-disable';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'This is a rescue command that can be used to turn off SAML settings in the event that you managed to lock yourself out using bad SAML settings.';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new command instance.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
if ($this->confirm("\n****************************************************\nThis will disable SAML support. You will not be able \nto login with an account that does not exist \nlocally in the Snipe-IT local database. \n****************************************************\n\nDo you wish to continue? [y|N]")) {
|
|
||||||
$setting = Setting::getSettings();
|
|
||||||
$setting->saml_enabled = 0;
|
|
||||||
if ($setting->save()) {
|
|
||||||
$this->info('SAML has been set to disabled.');
|
|
||||||
} else {
|
|
||||||
$this->info('Unable to disable SAML.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$this->info('Canceled. No actions taken.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Models\Accessory;
|
|
||||||
use App\Models\Actionlog;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class FixBulkAccessoryCheckinActionLogEntries extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'snipeit:fix-bulk-accessory-action-log-entries {--dry-run : Run the sync process but don\'t update the database} {--skip-backup : Skip pre-execution backup}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'This script attempts to fix timestamps and missing created_by values for bulk checkin entries in the log table';
|
|
||||||
|
|
||||||
private bool $dryrun = false;
|
|
||||||
private bool $skipBackup = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$this->skipBackup = $this->option('skip-backup');
|
|
||||||
$this->dryrun = $this->option('dry-run');
|
|
||||||
|
|
||||||
if ($this->dryrun) {
|
|
||||||
$this->info('This is a DRY RUN - no changes will be saved.');
|
|
||||||
$this->newLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
$logs = Actionlog::query()
|
|
||||||
// only look for accessory checkin logs
|
|
||||||
->where('item_type', Accessory::class)
|
|
||||||
// that were part of a bulk checkin
|
|
||||||
->where('note', 'Bulk checkin items')
|
|
||||||
// logs that were improperly timestamped should have created_at in the 1970s
|
|
||||||
->whereYear('created_at', '1970')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
if ($logs->isEmpty()) {
|
|
||||||
$this->info('No logs found with incorrect timestamps.');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->info('Found ' . $logs->count() . ' logs with incorrect timestamps:');
|
|
||||||
|
|
||||||
$this->table(
|
|
||||||
['ID', 'Created By', 'Created At', 'Updated At'],
|
|
||||||
$logs->map(function ($log) {
|
|
||||||
return [
|
|
||||||
$log->id,
|
|
||||||
$log->created_by,
|
|
||||||
$log->created_at,
|
|
||||||
$log->updated_at,
|
|
||||||
];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$this->dryrun && !$this->confirm('Update these logs?')) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->dryrun && !$this->skipBackup) {
|
|
||||||
$this->info('Backing up the database before making changes...');
|
|
||||||
$this->call('snipeit:backup');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->dryrun) {
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('DRY RUN. NOT ACTUALLY UPDATING LOGS.');
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($logs as $log) {
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('Processing log id:' . $log->id);
|
|
||||||
|
|
||||||
// created_by was not being set for accessory bulk checkins
|
|
||||||
// so let's see if there was another bulk checkin log
|
|
||||||
// with the same timestamp and a created_by value we can use.
|
|
||||||
if (is_null($log->created_by)) {
|
|
||||||
$createdByFromSimilarLog = $this->getCreatedByAttributeFromSimilarLog($log);
|
|
||||||
|
|
||||||
if ($createdByFromSimilarLog) {
|
|
||||||
$this->line(vsprintf('Updating log id:%s created_by to %s', [$log->id, $createdByFromSimilarLog]));
|
|
||||||
$log->created_by = $createdByFromSimilarLog;
|
|
||||||
} else {
|
|
||||||
$this->warn(vsprintf('No created_by found for log id:%s', [$log->id]));
|
|
||||||
$this->warn('Skipping updating this log since no similar log was found to update created_by from.');
|
|
||||||
|
|
||||||
// If we can't find a similar log then let's skip updating it
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->line(vsprintf('Updating log id:%s from %s to %s', [$log->id, $log->created_at, $log->updated_at]));
|
|
||||||
$log->created_at = $log->updated_at;
|
|
||||||
|
|
||||||
if (!$this->dryrun) {
|
|
||||||
Model::withoutTimestamps(function () use ($log) {
|
|
||||||
$log->saveQuietly();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->newLine();
|
|
||||||
|
|
||||||
if ($this->dryrun) {
|
|
||||||
$this->info('DRY RUN. NO CHANGES WERE ACTUALLY MADE.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hopefully the bulk checkin included other items like assets or licenses
|
|
||||||
* so we can use one of those logs to get the correct created_by value.
|
|
||||||
*
|
|
||||||
* This method attempts to find a bulk check in log that was
|
|
||||||
* created at the same time as the log passed in.
|
|
||||||
*/
|
|
||||||
private function getCreatedByAttributeFromSimilarLog(Actionlog $log): null|int
|
|
||||||
{
|
|
||||||
$similarLog = Actionlog::query()
|
|
||||||
->whereNotNull('created_by')
|
|
||||||
->where([
|
|
||||||
'action_type' => 'checkin from',
|
|
||||||
'note' => 'Bulk checkin items',
|
|
||||||
'target_id' => $log->target_id,
|
|
||||||
'target_type' => $log->target_type,
|
|
||||||
'created_at' => $log->updated_at,
|
|
||||||
])
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($similarLog) {
|
|
||||||
return $similarLog->created_by;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class FixUpAssignedTypeWithoutAssignedTo extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'snipeit:assigned-type-fixup';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Fixes up assets that have an assigned_type but no assigned_to';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
DB::table('assets')->whereNotNull('assigned_type')->whereNull('assigned_to')->update(['assigned_type' => null]);
|
|
||||||
$this->info("Assets with an assigned_type but no assigned_to are fixed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Helpers\Helper;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Laravel\Passport\TokenRepository;
|
use Laravel\Passport\TokenRepository;
|
||||||
|
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class GeneratePersonalAccessToken extends Command
|
class GeneratePersonalAccessToken extends Command
|
||||||
@@ -41,8 +43,9 @@ class GeneratePersonalAccessToken extends Command
|
|||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(TokenRepository $tokenRepository)
|
public function __construct(TokenRepository $tokenRepository, ValidationFactory $validation)
|
||||||
{
|
{
|
||||||
|
$this->validation = $validation;
|
||||||
$this->tokenRepository = $tokenRepository;
|
$this->tokenRepository = $tokenRepository;
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -73,7 +76,7 @@ class GeneratePersonalAccessToken extends Command
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
$this->warn('Your API Token has been created. Be sure to copy this token now, as it WILL NOT be accessible again.');
|
$this->warn('Your API Token has been created. Be sure to copy this token now, as it will not be accessible again.');
|
||||||
|
|
||||||
if ($token = DB::table('oauth_access_tokens')->where('user_id', '=', $user->id)->where('name','=',$accessTokenName)->orderBy('created_at', 'desc')->first()) {
|
if ($token = DB::table('oauth_access_tokens')->where('user_id', '=', $user->id)->where('name','=',$accessTokenName)->orderBy('created_at', 'desc')->first()) {
|
||||||
$this->info('API Token ID: '.$token->id);
|
$this->info('API Token ID: '.$token->id);
|
||||||
|
|||||||
@@ -55,8 +55,6 @@ class LdapSync extends Command
|
|||||||
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
|
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
|
||||||
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
|
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
|
||||||
|
|
||||||
|
|
||||||
// Map the LDAP attributes to the Snipe-IT user fields.
|
|
||||||
$ldap_map = [
|
$ldap_map = [
|
||||||
"username" => Setting::getSettings()->ldap_username_field,
|
"username" => Setting::getSettings()->ldap_username_field,
|
||||||
"last_name" => Setting::getSettings()->ldap_lname_field,
|
"last_name" => Setting::getSettings()->ldap_lname_field,
|
||||||
@@ -65,17 +63,11 @@ class LdapSync extends Command
|
|||||||
"emp_num" => Setting::getSettings()->ldap_emp_num,
|
"emp_num" => Setting::getSettings()->ldap_emp_num,
|
||||||
"email" => Setting::getSettings()->ldap_email,
|
"email" => Setting::getSettings()->ldap_email,
|
||||||
"phone" => Setting::getSettings()->ldap_phone_field,
|
"phone" => Setting::getSettings()->ldap_phone_field,
|
||||||
"mobile" => Setting::getSettings()->ldap_mobile,
|
|
||||||
"jobtitle" => Setting::getSettings()->ldap_jobtitle,
|
"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,
|
"country" => Setting::getSettings()->ldap_country,
|
||||||
"location" => Setting::getSettings()->ldap_location,
|
"location" => Setting::getSettings()->ldap_location,
|
||||||
"dept" => Setting::getSettings()->ldap_dept,
|
"dept" => Setting::getSettings()->ldap_dept,
|
||||||
"manager" => Setting::getSettings()->ldap_manager,
|
"manager" => Setting::getSettings()->ldap_manager,
|
||||||
"display_name" => Setting::getSettings()->ldap_display_name,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$ldap_default_group = Setting::getSettings()->ldap_default_group;
|
$ldap_default_group = Setting::getSettings()->ldap_default_group;
|
||||||
@@ -190,7 +182,7 @@ class LdapSync extends Command
|
|||||||
// Inject location information fields
|
// Inject location information fields
|
||||||
for ($i = 0; $i < $results['count']; $i++) {
|
for ($i = 0; $i < $results['count']; $i++) {
|
||||||
$results[$i]['ldap_location_override'] = false;
|
$results[$i]['ldap_location_override'] = false;
|
||||||
$results[$i]['location_id'] = null;
|
$results[$i]['location_id'] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab subsets based on location-specific DNs, and overwrite location for these users.
|
// Grab subsets based on location-specific DNs, and overwrite location for these users.
|
||||||
@@ -242,11 +234,9 @@ 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++) {
|
for ($i = 0; $i < $results['count']; $i++) {
|
||||||
$item = [];
|
$item = [];
|
||||||
$item['username'] = $results[$i][$ldap_map["username"]][0] ?? '';
|
$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['employee_number'] = $results[$i][$ldap_map["emp_num"]][0] ?? '';
|
||||||
$item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? '';
|
$item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? '';
|
||||||
$item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? '';
|
$item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? '';
|
||||||
@@ -254,13 +244,8 @@ class LdapSync extends Command
|
|||||||
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
|
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
|
||||||
$item['location_id'] = $results[$i]['location_id'] ?? '';
|
$item['location_id'] = $results[$i]['location_id'] ?? '';
|
||||||
$item['telephone'] = $results[$i][$ldap_map["phone"]][0] ?? '';
|
$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['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['country'] = $results[$i][$ldap_map["country"]][0] ?? '';
|
||||||
$item['zip'] = $results[$i][$ldap_map["zip"]][0] ?? '';
|
|
||||||
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
|
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
|
||||||
$item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? '';
|
$item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? '';
|
||||||
$item['location'] = $results[$i][$ldap_map["location"]][0] ?? '';
|
$item['location'] = $results[$i][$ldap_map["location"]][0] ?? '';
|
||||||
@@ -293,9 +278,6 @@ class LdapSync extends Command
|
|||||||
if($ldap_map["username"] != null){
|
if($ldap_map["username"] != null){
|
||||||
$user->username = $item['username'];
|
$user->username = $item['username'];
|
||||||
}
|
}
|
||||||
if($ldap_map["display_name"] != null){
|
|
||||||
$user->display_name = $item['display_name'];
|
|
||||||
}
|
|
||||||
if($ldap_map["last_name"] != null){
|
if($ldap_map["last_name"] != null){
|
||||||
$user->last_name = $item['lastname'];
|
$user->last_name = $item['lastname'];
|
||||||
}
|
}
|
||||||
@@ -311,27 +293,12 @@ class LdapSync extends Command
|
|||||||
if($ldap_map["phone"] != null){
|
if($ldap_map["phone"] != null){
|
||||||
$user->phone = $item['telephone'];
|
$user->phone = $item['telephone'];
|
||||||
}
|
}
|
||||||
if($ldap_map["mobile"] != null){
|
|
||||||
$user->mobile = $item['mobile'];
|
|
||||||
}
|
|
||||||
if($ldap_map["jobtitle"] != null){
|
if($ldap_map["jobtitle"] != null){
|
||||||
$user->jobtitle = $item['jobtitle'];
|
$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){
|
if($ldap_map["country"] != null){
|
||||||
$user->country = $item['country'];
|
$user->country = $item['country'];
|
||||||
}
|
}
|
||||||
if($ldap_map["zip"] != null){
|
|
||||||
$user->zip = $item['zip'];
|
|
||||||
}
|
|
||||||
if($ldap_map["dept"] != null){
|
if($ldap_map["dept"] != null){
|
||||||
$user->department_id = $department->id;
|
$user->department_id = $department->id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use Illuminate\Console\Command;
|
|||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Facades\Crypt;
|
use Illuminate\Support\Facades\Crypt;
|
||||||
use App\Models\Ldap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a given ip is in a network
|
* Check if a given ip is in a network
|
||||||
@@ -161,15 +160,7 @@ class LdapTroubleshooter extends Command
|
|||||||
$output[] = "-x";
|
$output[] = "-x";
|
||||||
$output[] = "-b ".escapeshellarg($settings->ldap_basedn);
|
$output[] = "-b ".escapeshellarg($settings->ldap_basedn);
|
||||||
$output[] = "-D ".escapeshellarg($settings->ldap_uname);
|
$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));
|
$output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter));
|
||||||
if($settings->ldap_tls) {
|
if($settings->ldap_tls) {
|
||||||
$this->line("# adding STARTTLS option");
|
$this->line("# adding STARTTLS option");
|
||||||
@@ -180,23 +171,6 @@ class LdapTroubleshooter extends Command
|
|||||||
$this->line(implode(" \\\n",$output));
|
$this->line(implode(" \\\n",$output));
|
||||||
exit(0);
|
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')) {
|
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?');
|
$confirmation = $this->confirm('WARNING: This command will make several attempts to connect to your LDAP server. Are you sure this is ok?');
|
||||||
if(!$confirmation) {
|
if(!$confirmation) {
|
||||||
@@ -205,7 +179,7 @@ class LdapTroubleshooter extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//$this->line(print_r($settings,true));
|
//$this->line(print_r($settings,true));
|
||||||
$this->line("STAGE 1: Checking settings");
|
$this->info("STAGE 1: Checking settings");
|
||||||
if(!$settings->ldap_enabled) {
|
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)");
|
$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)");
|
||||||
}
|
}
|
||||||
@@ -236,40 +210,32 @@ class LdapTroubleshooter extends Command
|
|||||||
$this->info("Determined LDAP hostname to be: ".$parsed['host']);
|
$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 = [];
|
$raw_ips = [];
|
||||||
|
|
||||||
if (inet_pton($parsed['host']) !== false) {
|
//$this->info("Host IP is: ".print_r($ips,true));
|
||||||
$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']);
|
|
||||||
|
|
||||||
//$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.");
|
||||||
if (!$ips || count($ips) == 0) {
|
exit(-1);
|
||||||
$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;
|
|
||||||
}
|
|
||||||
$raw_ips[] = $ip['ip'];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
foreach ($raw_ips as $ip) {
|
$this->debugout("IP's? ".print_r($ips,true));
|
||||||
if ($ip == "127.0.0.1") {
|
foreach($ips as $ip) {
|
||||||
|
if(!isset($ip['ip'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$raw_ips[]=$ip['ip'];
|
||||||
|
if($ip['ip'] == "127.0.0.1") {
|
||||||
$this->error("WARNING: Using the localhost IP as the LDAP server. This is usually wrong");
|
$this->error("WARNING: Using the localhost IP as the LDAP server. This is usually wrong");
|
||||||
}
|
}
|
||||||
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')) {
|
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')) {
|
||||||
$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->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->line("STAGE 2: Checking basic network connectivity");
|
$this->info("STAGE 2: Checking basic network connectivity");
|
||||||
$ports = [636, 389];
|
$ports = [389,636];
|
||||||
if(@$parsed['port'] && !in_array($parsed['port'],$ports)) {
|
if(@$parsed['port'] && !in_array($parsed['port'],$ports)) {
|
||||||
$ports[] = $parsed['port'];
|
$ports[] = $parsed['port'];
|
||||||
}
|
}
|
||||||
@@ -280,7 +246,7 @@ class LdapTroubleshooter extends Command
|
|||||||
$errstr = '';
|
$errstr = '';
|
||||||
$timeout = 30.0;
|
$timeout = 30.0;
|
||||||
$result = '';
|
$result = '';
|
||||||
$this->line("Attempting to connect to port: " . $port . " - may take up to $timeout seconds");
|
$this->info("Attempting to connect to port: ".$port." - may take up to $timeout seconds");
|
||||||
try {
|
try {
|
||||||
$result = fsockopen($parsed['host'], $port, $errno, $errstr, 30.0);
|
$result = fsockopen($parsed['host'], $port, $errno, $errstr, 30.0);
|
||||||
} catch(Exception $e) {
|
} catch(Exception $e) {
|
||||||
@@ -299,9 +265,9 @@ class LdapTroubleshooter extends Command
|
|||||||
exit(-1);
|
exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->line("STAGE 3: Determine encryption algorithm, if any");
|
$this->info("STAGE 3: Determine encryption algorithm, if any");
|
||||||
|
|
||||||
$ldap_urls = []; // [url, cert-check?, start_tls?]
|
$ldap_urls = [];
|
||||||
$pretty_ldap_urls = [];
|
$pretty_ldap_urls = [];
|
||||||
foreach($open_ports as $port) {
|
foreach($open_ports as $port) {
|
||||||
$this->line("Trying TLS first for port $port");
|
$this->line("Trying TLS first for port $port");
|
||||||
@@ -309,46 +275,35 @@ class LdapTroubleshooter extends Command
|
|||||||
if($this->test_anonymous_bind($ldap_url)) {
|
if($this->test_anonymous_bind($ldap_url)) {
|
||||||
$this->info("Anonymous bind succesful to $ldap_url!");
|
$this->info("Anonymous bind succesful to $ldap_url!");
|
||||||
$ldap_urls[] = [ $ldap_url, true, false ];
|
$ldap_urls[] = [ $ldap_url, true, false ];
|
||||||
$pretty_ldap_urls[] = [$ldap_url, "enabled", "n/a (no)"];
|
$pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ];
|
||||||
continue; // TODO - lots of copypasta in these if(test_anonymous_bind()) routines...
|
continue; // TODO - lots of copypasta in these if(test_anonymous_bind()) routines...
|
||||||
} else {
|
} else {
|
||||||
$this->error("WARNING: Failed to bind to $ldap_url - trying without certificate checks.");
|
$this->error("WARNING: Failed to bind to $ldap_url - trying without certificate checks.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if($this->test_anonymous_bind($ldap_url, false)) {
|
if($this->test_anonymous_bind($ldap_url, false)) {
|
||||||
$this->info("Anonymous bind successful to $ldap_url with certificate-checks disabled");
|
$this->info("Anonymous bind succesful to $ldap_url with certifcate-checks disabled");
|
||||||
$ldap_urls[] = [$ldap_url, false, false];
|
$ldap_urls[] = [ $ldap_url, false, false ];
|
||||||
$pretty_ldap_urls[] = [$ldap_url, "DISABLED", "n/a (no)"];
|
$pretty_ldap_urls[] = [ $ldap_url, "no", "no" ];
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
$this->error("WARNING: Failed to bind to $ldap_url with certificate checks disabled. Trying unencrypted with STARTTLS");
|
$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";
|
$ldap_url = "ldap://".$parsed['host'].":$port";
|
||||||
|
|
||||||
if($this->test_anonymous_bind($ldap_url, true, true)) {
|
if($this->test_anonymous_bind($ldap_url, true, true)) {
|
||||||
$this->info("Plain connection to $ldap_url with STARTTLS succesful!");
|
$this->info("Plain connection to $ldap_url with STARTTLS succesful!");
|
||||||
$ldap_urls[] = [ $ldap_url, true, true ];
|
$ldap_urls[] = [ $ldap_url, true, true ];
|
||||||
$pretty_ldap_urls[] = [$ldap_url, "enabled", "STARTTLS ENABLED"];
|
$pretty_ldap_urls[] = [ $ldap_url, "YES", "YES" ];
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without certificate checks.");
|
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without STARTTLS");
|
||||||
}
|
|
||||||
|
|
||||||
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)) {
|
if($this->test_anonymous_bind($ldap_url)) {
|
||||||
$this->info("Plain connection to $ldap_url succesful!");
|
$this->info("Plain connection to $ldap_url succesful!");
|
||||||
$ldap_urls[] = [ $ldap_url, true, false ];
|
$ldap_urls[] = [ $ldap_url, true, false ];
|
||||||
$pretty_ldap_urls[] = [$ldap_url, "n/a", "starttls disabled"];
|
$pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ];
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
$this->error("WARNING: Failed to bind to $ldap_url. Giving up on port $port");
|
$this->error("WARNING: Failed to bind to $ldap_url. Giving up on port $port");
|
||||||
@@ -358,29 +313,23 @@ class LdapTroubleshooter extends Command
|
|||||||
$this->debugout(print_r($ldap_urls,true));
|
$this->debugout(print_r($ldap_urls,true));
|
||||||
|
|
||||||
if(count($ldap_urls) > 0 ) {
|
if(count($ldap_urls) > 0 ) {
|
||||||
$this->debugout("Found working LDAP URL's: ");
|
$this->info("Found working LDAP URL's: ");
|
||||||
foreach($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead?
|
foreach($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead?
|
||||||
$this->debugout("LDAP URL: " . $ldap_url[0]);
|
$this->info("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->info($ldap_url[0]. ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled"). ($ldap_url[2] ? " STARTTLS Enabled ": " STARTTLS Disabled"));
|
||||||
}
|
}
|
||||||
$this->table(["URL", "Cert Checks?", "STARTTLS?"], $pretty_ldap_urls);
|
$this->table(["URL", "Cert Checks Enabled?", "STARTTLS Enabled?"],$pretty_ldap_urls);
|
||||||
} else {
|
} else {
|
||||||
$this->error("ERROR - no valid LDAP URL's available - ABORTING");
|
$this->error("ERROR - no valid LDAP URL's available - ABORTING");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->line("STAGE 4: Test Administrative Bind for LDAP Sync");
|
$this->info("STAGE 4: Test Administrative Bind for LDAP Sync");
|
||||||
foreach($ldap_urls AS $ldap_url) {
|
foreach($ldap_urls AS $ldap_url) {
|
||||||
try {
|
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, Crypt::decrypt($settings->ldap_pword));
|
||||||
$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->line("STAGE 5: Test BaseDN");
|
$this->info("STAGE 5: Test BaseDN");
|
||||||
//grab all LDAP_ constants and fill up a reversed array mapping from weird LDAP dotted-strings to (Constant Name)
|
//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();
|
$all_defined_constants = get_defined_constants();
|
||||||
$ldap_constants = [];
|
$ldap_constants = [];
|
||||||
@@ -392,23 +341,16 @@ class LdapTroubleshooter extends Command
|
|||||||
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
|
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
|
||||||
|
|
||||||
foreach($ldap_urls AS $ldap_url) {
|
foreach($ldap_urls AS $ldap_url) {
|
||||||
try {
|
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,Crypt::decrypt($settings->ldap_pword),$settings)) {
|
||||||
$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!");
|
$this->info("Success getting informational bind!");
|
||||||
} else {
|
} else {
|
||||||
$this->error("Unable to get information from bind.");
|
$this->error("Unable to get information from bind.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->line("STAGE 6: Test LDAP Login to Snipe-IT");
|
$this->info("STAGE 6: Test LDAP Login to Snipe-IT");
|
||||||
foreach($ldap_urls AS $ldap_url) {
|
foreach($ldap_urls AS $ldap_url) {
|
||||||
$this->line("Starting auth to " . $ldap_url[0]);
|
$this->info("Starting auth to ".$ldap_url[0]);
|
||||||
while(true) {
|
while(true) {
|
||||||
$with_tls = $ldap_url[1] ? "with": "without";
|
$with_tls = $ldap_url[1] ? "with": "without";
|
||||||
$with_startssl = $ldap_url[2] ? "using": "not using";
|
$with_startssl = $ldap_url[2] ? "using": "not using";
|
||||||
@@ -417,12 +359,7 @@ class LdapTroubleshooter extends Command
|
|||||||
}
|
}
|
||||||
$username = $this->ask("Username");
|
$username = $this->ask("Username");
|
||||||
$password = $this->secret("Password");
|
$password = $this->secret("Password");
|
||||||
$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?
|
$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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,17 +368,14 @@ class LdapTroubleshooter extends Command
|
|||||||
|
|
||||||
public function connect_to_ldap($ldap_url, $check_cert, $start_tls)
|
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);
|
$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?
|
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
|
// 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) {
|
if($this->settings->ldap_client_tls_cert && $this->settings->ldap_client_tls_key) {
|
||||||
// client-side TLS certificate support for LDAP (Google Secure LDAP)
|
// client-side TLS certificate support for LDAP (Google Secure LDAP)
|
||||||
putenv('LDAPTLS_CERT=storage/ldap_client_tls.cert');
|
putenv('LDAPTLS_CERT=storage/ldap_client_tls.cert');
|
||||||
@@ -470,10 +404,9 @@ class LdapTroubleshooter extends Command
|
|||||||
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert , $start_tls) {
|
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert , $start_tls) {
|
||||||
try {
|
try {
|
||||||
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
|
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
|
||||||
$this->line("Attempting to bind now, this can take a while if we mess it up");
|
$this->info("gonna try to bind now, this can take a while if we mess it up");
|
||||||
$bind_results = ldap_bind($lconn);
|
$bind_results = ldap_bind($lconn);
|
||||||
$this->line("Bind results are: " . $bind_results . " which translate into boolean: " . (bool)$bind_results);
|
$this->info("Bind results are: ".$bind_results." which translate into boolean: ".(bool)$bind_results);
|
||||||
ldap_close($lconn);
|
|
||||||
return (bool)$bind_results;
|
return (bool)$bind_results;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->error("WARNING: Exception caught during bind - ".$e->getMessage());
|
$this->error("WARNING: Exception caught during bind - ".$e->getMessage());
|
||||||
@@ -488,7 +421,6 @@ class LdapTroubleshooter extends Command
|
|||||||
try {
|
try {
|
||||||
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
|
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
|
||||||
$bind_results = ldap_bind($lconn, $username, $password);
|
$bind_results = ldap_bind($lconn, $username, $password);
|
||||||
ldap_close($lconn);
|
|
||||||
if(!$bind_results) {
|
if(!$bind_results) {
|
||||||
$this->error("WARNING: Failed to bind to $ldap_url as $username");
|
$this->error("WARNING: Failed to bind to $ldap_url as $username");
|
||||||
return false;
|
return false;
|
||||||
@@ -514,62 +446,22 @@ class LdapTroubleshooter extends Command
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
|
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
|
||||||
$cleaned_results = [];
|
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
|
||||||
try {
|
$results = ldap_get_entries($conn, $result);
|
||||||
// This _may_ only work for Active Directory?
|
$cleaned_results = $this->ldap_results_cleaner($results);
|
||||||
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
|
$this->line(print_r($cleaned_results,true));
|
||||||
$results = ldap_get_entries($conn, $result);
|
//okay, great - now how do we display those results? I have no idea.
|
||||||
$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?
|
// I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it?
|
||||||
$this->debugout("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
|
$this->comment("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));
|
$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));
|
$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: ");
|
$this->info("Printing first 10 results: ");
|
||||||
$pretty_data = array_slice($this->ldap_results_cleaner($entries), 0, 10);
|
for($i=0;$i<10;$i++) {
|
||||||
//print_r($data);
|
$this->info($search_results[$i]);
|
||||||
$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) {
|
} catch (\Exception $e) {
|
||||||
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
|
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
|
||||||
ldap_close($conn);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -585,7 +477,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'))) {
|
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)
|
// POSIX functions needed for forking aren't present, just run the function inline (ignoring timeout)
|
||||||
$this->line('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
|
$this->info('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
|
||||||
return $function();
|
return $function();
|
||||||
} else {
|
} else {
|
||||||
$parent_pid = posix_getpid();
|
$parent_pid = posix_getpid();
|
||||||
@@ -622,6 +514,4 @@ class LdapTroubleshooter extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class MoveUploadsToNewDisk extends Command
|
|||||||
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
|
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
|
||||||
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
|
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
|
||||||
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
|
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
|
||||||
$private_uploads['assetmodels'] = glob('storage/private_uploads/models'."/*.*");
|
$private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'."/*.*");
|
||||||
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
|
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
|
||||||
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
|
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
|
||||||
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");
|
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use Symfony\Component\Console\Input\InputOption;
|
|||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Symfony\Component\Console\Helper\ProgressIndicator;
|
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
|
* Class ObjectImportCommand
|
||||||
@@ -50,9 +52,6 @@ class ObjectImportCommand extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle()
|
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);
|
$this->progressIndicator = new ProgressIndicator($this->output);
|
||||||
|
|
||||||
$filename = $this->argument('filename');
|
$filename = $this->argument('filename');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace App\Console\Commands;
|
|||||||
|
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\CustomField;
|
use App\Models\CustomField;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Schema;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
@@ -51,7 +51,8 @@ class PaveIt extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List all the tables in the database so we don't have to worry about missing some as the app grows
|
// List all the tables in the database so we don't have to worry about missing some as the app grows
|
||||||
$tables = Schema::getTables();
|
$tables = DB::connection()->getDoctrineSchemaManager()->listTableNames();
|
||||||
|
|
||||||
$except_tables = [
|
$except_tables = [
|
||||||
'oauth_access_tokens',
|
'oauth_access_tokens',
|
||||||
'oauth_clients',
|
'oauth_clients',
|
||||||
@@ -59,9 +60,6 @@ class PaveIt extends Command
|
|||||||
'migrations',
|
'migrations',
|
||||||
'settings',
|
'settings',
|
||||||
'users',
|
'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.
|
// We only need to find out what these are so we can nuke these columns on the assets table.
|
||||||
@@ -69,15 +67,14 @@ class PaveIt extends Command
|
|||||||
foreach ($custom_fields as $custom_field) {
|
foreach ($custom_fields as $custom_field) {
|
||||||
$this->info('DROP the '.$custom_field->db_column.' column from assets as well.');
|
$this->info('DROP the '.$custom_field->db_column.' column from assets as well.');
|
||||||
|
|
||||||
if (Schema::hasColumn('assets', $custom_field->db_column)) {
|
if (\Schema::hasColumn('assets', $custom_field->db_column)) {
|
||||||
Schema::table('assets', function ($table) use ($custom_field) {
|
\Schema::table('assets', function ($table) use ($custom_field) {
|
||||||
$table->dropColumn($custom_field->db_column);
|
$table->dropColumn($custom_field->db_column);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($tables as $table_obj) {
|
foreach ($tables as $table) {
|
||||||
$table = $table_obj['name'];
|
|
||||||
if (in_array($table, $except_tables)) {
|
if (in_array($table, $except_tables)) {
|
||||||
$this->info($table. ' is SKIPPED.');
|
$this->info($table. ' is SKIPPED.');
|
||||||
} else {
|
} else {
|
||||||
@@ -87,8 +84,8 @@ class PaveIt extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Leave in the demo oauth keys so we don't have to reset them every day in the demos
|
// 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_clients WHERE id > 2');
|
||||||
DB::statement('delete from oauth_access_tokens WHERE user_id > 2');
|
\DB::statement('delete from oauth_access_tokens WHERE id > 2');
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,19 +62,19 @@ class Purge extends Command
|
|||||||
$assetcount = $assets->count();
|
$assetcount = $assets->count();
|
||||||
$this->info($assets->count().' assets purged.');
|
$this->info($assets->count().' assets purged.');
|
||||||
$asset_assoc = 0;
|
$asset_assoc = 0;
|
||||||
$maintenances = 0;
|
$asset_maintenances = 0;
|
||||||
|
|
||||||
foreach ($assets as $asset) {
|
foreach ($assets as $asset) {
|
||||||
$this->info('- Asset "'.$asset->display_name.'" deleted.');
|
$this->info('- Asset "'.$asset->present()->name().'" deleted.');
|
||||||
$asset_assoc += $asset->assetlog()->count();
|
$asset_assoc += $asset->assetlog()->count();
|
||||||
$asset->assetlog()->forceDelete();
|
$asset->assetlog()->forceDelete();
|
||||||
$maintenances += $asset->maintenances()->count();
|
$asset_maintenances += $asset->assetmaintenances()->count();
|
||||||
$asset->maintenances()->forceDelete();
|
$asset->assetmaintenances()->forceDelete();
|
||||||
$asset->forceDelete();
|
$asset->forceDelete();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->info($asset_assoc.' corresponding log records purged.');
|
$this->info($asset_assoc.' corresponding log records purged.');
|
||||||
$this->info($maintenances.' corresponding maintenance records purged.');
|
$this->info($asset_maintenances.' corresponding maintenance records purged.');
|
||||||
|
|
||||||
$locations = Location::whereNotNull('deleted_at')->withTrashed()->get();
|
$locations = Location::whereNotNull('deleted_at')->withTrashed()->get();
|
||||||
$this->info($locations->count().' locations purged.');
|
$this->info($locations->count().' locations purged.');
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
<?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,7 +5,6 @@ namespace App\Console\Commands;
|
|||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use ZipArchive;
|
use ZipArchive;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use enshrined\svgSanitize\Sanitizer;
|
|
||||||
|
|
||||||
class SQLStreamer {
|
class SQLStreamer {
|
||||||
private $input;
|
private $input;
|
||||||
@@ -243,17 +242,13 @@ class RestoreFromBackup extends Command
|
|||||||
|
|
||||||
$private_dirs = [
|
$private_dirs = [
|
||||||
'storage/private_uploads/accessories',
|
'storage/private_uploads/accessories',
|
||||||
'storage/private_uploads/assetmodels' => 'storage/private_uploads/models', //this was changed from assetmodels => models Aug 10 2025
|
'storage/private_uploads/assetmodels',
|
||||||
'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/assets', // these are asset _files_, not the pictures.
|
||||||
'storage/private_uploads/audits',
|
'storage/private_uploads/audits',
|
||||||
'storage/private_uploads/components',
|
'storage/private_uploads/components',
|
||||||
'storage/private_uploads/consumables',
|
'storage/private_uploads/consumables',
|
||||||
'storage/private_uploads/eula-pdfs',
|
'storage/private_uploads/eula-pdfs',
|
||||||
'storage/private_uploads/imports',
|
'storage/private_uploads/imports',
|
||||||
'storage/private_uploads/locations',
|
|
||||||
'storage/private_uploads/licenses',
|
'storage/private_uploads/licenses',
|
||||||
'storage/private_uploads/signatures',
|
'storage/private_uploads/signatures',
|
||||||
'storage/private_uploads/users',
|
'storage/private_uploads/users',
|
||||||
@@ -264,10 +259,9 @@ class RestoreFromBackup extends Command
|
|||||||
];
|
];
|
||||||
$public_dirs = [
|
$public_dirs = [
|
||||||
'public/uploads/accessories',
|
'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/assets', // these are asset _pictures_, not asset files
|
||||||
'public/uploads/avatars',
|
'public/uploads/avatars',
|
||||||
|
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
|
||||||
'public/uploads/categories',
|
'public/uploads/categories',
|
||||||
'public/uploads/companies',
|
'public/uploads/companies',
|
||||||
'public/uploads/components',
|
'public/uploads/components',
|
||||||
@@ -275,7 +269,7 @@ class RestoreFromBackup extends Command
|
|||||||
'public/uploads/departments',
|
'public/uploads/departments',
|
||||||
'public/uploads/locations',
|
'public/uploads/locations',
|
||||||
'public/uploads/manufacturers',
|
'public/uploads/manufacturers',
|
||||||
'public/uploads/models', // ...it's been this way for 9 years (as of late 2025)
|
'public/uploads/models',
|
||||||
'public/uploads/suppliers',
|
'public/uploads/suppliers',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -288,27 +282,14 @@ class RestoreFromBackup extends Command
|
|||||||
'public/uploads/favicon-uploaded.*',
|
'public/uploads/favicon-uploaded.*',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$all_files = $private_dirs + $public_dirs;
|
||||||
|
|
||||||
$sqlfiles = [];
|
$sqlfiles = [];
|
||||||
$sqlfile_indices = [];
|
$sqlfile_indices = [];
|
||||||
|
|
||||||
$interesting_files = [];
|
$interesting_files = [];
|
||||||
$boring_files = [];
|
$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++) {
|
for ($i = 0; $i < $za->numFiles; $i++) {
|
||||||
$stat_results = $za->statIndex($i);
|
$stat_results = $za->statIndex($i);
|
||||||
// echo "index: $i\n";
|
// echo "index: $i\n";
|
||||||
@@ -323,7 +304,7 @@ class RestoreFromBackup extends Command
|
|||||||
// skip macOS resource fork files (?!?!?!)
|
// skip macOS resource fork files (?!?!?!)
|
||||||
if (strpos($raw_path, '__MACOSX') !== false && strpos($raw_path, '._') !== false) {
|
if (strpos($raw_path, '__MACOSX') !== false && strpos($raw_path, '._') !== false) {
|
||||||
//print "SKIPPING macOS Resource fork file: $raw_path\n";
|
//print "SKIPPING macOS Resource fork file: $raw_path\n";
|
||||||
// $boring_files[] = $raw_path; //stop adding this to the boring files list; it's just confusing
|
$boring_files[] = $raw_path;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') {
|
if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') {
|
||||||
@@ -332,70 +313,41 @@ class RestoreFromBackup extends Command
|
|||||||
$sqlfile_indices[] = $i;
|
$sqlfile_indices[] = $i;
|
||||||
continue;
|
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 (['public' => $public_dirs, 'private' => $private_dirs] as $purpose => $dirs) {
|
foreach (array_merge($private_dirs, $public_dirs) as $dir) {
|
||||||
$allowed_extensions = match ($purpose) {
|
$last_pos = strrpos($raw_path, $dir . '/');
|
||||||
'public' => $public_extensions,
|
if ($last_pos !== false) {
|
||||||
'private' => $private_extensions,
|
//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?
|
||||||
foreach ($dirs as $dir => $destdir) {
|
$interesting_files[$raw_path] = ['dest' => $dir, 'index' => $i];
|
||||||
if (is_int($dir)) {
|
continue 2;
|
||||||
$dir = $destdir;
|
if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) {
|
||||||
}
|
// we don't care about that; we just want files with the appropriate prefix
|
||||||
$last_pos = strrpos($raw_path, $dir . '/');
|
//print("FOUND THE EXACT DIRECTORY: $dir AT: $raw_path!!!\n");
|
||||||
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',
|
||||||
foreach (['public' => $public_files, 'private' => $private_files] as $purpose => $files) {
|
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico',];
|
||||||
$allowed_extensions = match ($purpose) {
|
foreach (array_merge($private_files, $public_files) as $file) {
|
||||||
'public' => $public_extensions,
|
$has_wildcard = (strpos($file, '*') !== false);
|
||||||
'private' => $private_extensions,
|
if ($has_wildcard) {
|
||||||
};
|
$file = substr($file, 0, -1); //trim last character (which should be the wildcard)
|
||||||
foreach ($files as $file) {
|
}
|
||||||
$has_wildcard = (strpos($file, '*') !== false);
|
$last_pos = strrpos($raw_path, $file); // no trailing slash!
|
||||||
if ($has_wildcard) {
|
if ($last_pos !== false) {
|
||||||
$file = substr($file, 0, -1); //trim last character (which should be the wildcard)
|
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
|
||||||
|
if (!in_array($extension, $good_extensions)) {
|
||||||
|
$this->warn('Potentially unsafe file ' . $raw_path . ' is being skipped');
|
||||||
|
$boring_files[] = $raw_path;
|
||||||
|
continue 2;
|
||||||
}
|
}
|
||||||
$last_pos = strrpos($raw_path, $file); // no trailing slash!
|
//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");
|
||||||
if ($last_pos !== false) {
|
//no wildcards found in $file, process 'normally'
|
||||||
if (!in_array($extension, $allowed_extensions)) {
|
if ($last_pos + strlen($file) == strlen($raw_path) || $has_wildcard) { //again, no trailing slash. or this is a wildcard and we just take it.
|
||||||
// gathering potentially unsafe files here to return at exit
|
// print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
|
||||||
$unsafe_files[] = $raw_path;
|
$interesting_files[$raw_path] = ['dest' => dirname($file), 'index' => $i];
|
||||||
Log::debug('Potentially unsafe file ' . $raw_path . ' is being skipped');
|
continue 2;
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,7 +372,6 @@ class RestoreFromBackup extends Command
|
|||||||
if ($this->option('sanitize-guess-prefix')) {
|
if ($this->option('sanitize-guess-prefix')) {
|
||||||
$prefix = SQLStreamer::guess_prefix($sql_contents);
|
$prefix = SQLStreamer::guess_prefix($sql_contents);
|
||||||
$this->line($prefix);
|
$this->line($prefix);
|
||||||
|
|
||||||
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitize your SQL.");
|
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitize your SQL.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,25 +483,18 @@ class RestoreFromBackup extends Command
|
|||||||
}
|
}
|
||||||
foreach ($interesting_files as $pretty_file_name => $file_details) {
|
foreach ($interesting_files as $pretty_file_name => $file_details) {
|
||||||
$ugly_file_name = $za->statIndex($file_details['index'])['name'];
|
$ugly_file_name = $za->statIndex($file_details['index'])['name'];
|
||||||
$migrated_file_name = $file_details['dest'] . '/' . basename($pretty_file_name);
|
$fp = $za->getStream($ugly_file_name);
|
||||||
if (strcasecmp(substr($pretty_file_name, -4), ".svg") === 0) {
|
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
|
||||||
$svg_contents = $za->getFromIndex($file_details['index']);
|
if (!is_dir($file_details['dest'])) {
|
||||||
$cleaned_svg = $sanitizer->sanitize($svg_contents);
|
mkdir($file_details['dest'], 0755, true); //0755 is what Laravel uses, so we do that
|
||||||
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) {
|
if ($bar) {
|
||||||
$bar->advance();
|
$bar->advance();
|
||||||
}
|
}
|
||||||
@@ -561,11 +505,6 @@ class RestoreFromBackup extends Command
|
|||||||
} else {
|
} else {
|
||||||
$this->info(count($interesting_files).' files were succesfully transferred');
|
$this->info(count($interesting_files).' files were succesfully transferred');
|
||||||
}
|
}
|
||||||
if (count($unsafe_files) > 0) {
|
|
||||||
foreach ($unsafe_files as $unsafe_file) {
|
|
||||||
$this->warn('Potentially unsafe file '.$unsafe_file.' was skipped');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($boring_files as $boring_file) {
|
foreach ($boring_files as $boring_file) {
|
||||||
$this->warn($boring_file.' was skipped.');
|
$this->warn($boring_file.' was skipped.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class SendAcceptanceReminder extends Command
|
|||||||
if(!$email){
|
if(!$email){
|
||||||
$no_email_list[] = [
|
$no_email_list[] = [
|
||||||
'id' => $acceptance->assignedTo?->id,
|
'id' => $acceptance->assignedTo?->id,
|
||||||
'name' => $acceptance->assignedTo?->display_name,
|
'name' => $acceptance->assignedTo?->present()->fullName(),
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
$count++;
|
$count++;
|
||||||
@@ -99,11 +99,8 @@ class SendAcceptanceReminder extends Command
|
|||||||
foreach ($no_email_list as $user) {
|
foreach ($no_email_list as $user) {
|
||||||
$rows[] = [$user['id'], $user['name']];
|
$rows[] = [$user['id'], $user['name']];
|
||||||
}
|
}
|
||||||
|
$this->info("The following users do not have an email address:");
|
||||||
if (!empty($rows)) {
|
$this->table($headers, $rows);
|
||||||
$this->info("The following users do not have an email address:");
|
|
||||||
$this->table($headers, $rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Helpers\Helper;
|
|
||||||
use App\Mail\ExpiringAssetsMail;
|
use App\Mail\ExpiringAssetsMail;
|
||||||
use App\Mail\ExpiringLicenseMail;
|
use App\Mail\ExpiringLicenseMail;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
@@ -43,83 +42,28 @@ class SendExpirationAlerts extends Command
|
|||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
$settings = Setting::getSettings();
|
$settings = Setting::getSettings();
|
||||||
$alert_interval = $settings->alert_interval;
|
$threshold = $settings->alert_interval;
|
||||||
|
|
||||||
if (($settings->alert_email != '') && ($settings->alerts_enabled == 1)) {
|
if (($settings->alert_email != '') && ($settings->alerts_enabled == 1)) {
|
||||||
|
|
||||||
// Send a rollup to the admin, if settings dictate
|
// Send a rollup to the admin, if settings dictate
|
||||||
$recipients = collect(explode(',', $settings->alert_email))
|
$recipients = collect(explode(',', $settings->alert_email))
|
||||||
->map(fn($item) => trim($item)) // Trim each email
|
->map(fn($item) => trim($item)) // Trim each email
|
||||||
->filter(fn($item) => !empty($item))
|
|
||||||
->all();
|
->all();
|
||||||
// Expiring Assets
|
// Expiring Assets
|
||||||
$assets = Asset::getExpiringWarrantyOrEol($alert_interval);
|
$assets = Asset::getExpiringWarrantee($threshold);
|
||||||
|
|
||||||
if ($assets->count() > 0) {
|
if ($assets->count() > 0) {
|
||||||
|
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $threshold]));
|
||||||
Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval));
|
Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $threshold));
|
||||||
|
|
||||||
$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
|
// Expiring licenses
|
||||||
$licenses = License::query()->ExpiringLicenses($alert_interval)
|
$licenses = License::getExpiringLicenses($threshold);
|
||||||
->with('manufacturer','category')
|
|
||||||
->orderBy('expiration_date', 'ASC')
|
|
||||||
->orderBy('termination_date', 'ASC')
|
|
||||||
->get();
|
|
||||||
if ($licenses->count() > 0) {
|
if ($licenses->count() > 0) {
|
||||||
Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $alert_interval));
|
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $threshold]));
|
||||||
|
Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $threshold));
|
||||||
$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 {
|
} else {
|
||||||
if ($settings->alert_email == '') {
|
if ($settings->alert_email == '') {
|
||||||
$this->error('Could not send email. No alert email configured in settings');
|
$this->error('Could not send email. No alert email configured in settings');
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class SendUpcomingAuditReport extends Command
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $signature = 'snipeit:upcoming-audits {--with-output : Display the results in a table in your console in addition to sending the email}';
|
protected $signature = 'snipeit:upcoming-audits';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The console command description.
|
* The console command description.
|
||||||
@@ -47,69 +47,20 @@ class SendUpcomingAuditReport extends Command
|
|||||||
$today = Carbon::now();
|
$today = Carbon::now();
|
||||||
$interval_date = $today->copy()->addDays($interval);
|
$interval_date = $today->copy()->addDays($interval);
|
||||||
|
|
||||||
$assets_query = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'asc')->with('supplier');
|
$assets = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get();
|
||||||
$asset_count = $assets_query->count();
|
$this->info($assets->count() . ' assets must be audited in on or before ' . $interval_date . ' is deadline');
|
||||||
$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) {
|
if ((count($assets) !== 0) && ($assets->count() > 0) && ($settings->alert_email != '')) {
|
||||||
|
|
||||||
$assets_for_email = $assets_query->limit(30)->get();
|
|
||||||
|
|
||||||
// Send a rollup to the admin, if settings dictate
|
// Send a rollup to the admin, if settings dictate
|
||||||
if ($settings->alert_email != '') {
|
$recipients = collect(explode(',', $settings->alert_email))
|
||||||
|
->map(fn($item) => trim($item))
|
||||||
$recipients = collect(explode(',', $settings->alert_email))
|
->all();
|
||||||
->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.');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
$this->info('Sending Admin SendUpcomingAuditNotification to: ' . $settings->alert_email);
|
||||||
if ($this->option('with-output')) {
|
Mail::to($recipients)->send(new SendUpcomingAuditMail($assets, $settings->audit_warning_days));
|
||||||
|
|
||||||
|
|
||||||
// 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,8 +37,6 @@ class SystemBackup extends Command
|
|||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
ini_set('max_execution_time', env('BACKUP_TIME_LIMIT', 600)); //600 seconds = 10 minutes
|
|
||||||
|
|
||||||
if ($this->option('filename')) {
|
if ($this->option('filename')) {
|
||||||
$filename = $this->option('filename');
|
$filename = $this->option('filename');
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use App\Helpers\Helper;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class TestLocationsFMCS extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'snipeit:test-locations-fmcs {--location_id=}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Test for company ID inconsistencies if FullMultipleCompanySupport with scoped locations will be used.';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$this->info('This script checks for company ID inconsistencies if Full Multiple Company Support with scoped locations will be used.');
|
|
||||||
$this->info('This could take a few moments if have a very large dataset.');
|
|
||||||
$this->newLine();
|
|
||||||
|
|
||||||
// if parameter location_id is set, only test this location
|
|
||||||
$location_id = null;
|
|
||||||
if ($this->option('location_id')) {
|
|
||||||
$location_id = $this->option('location_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
$mismatched = Helper::test_locations_fmcs(true, $location_id);
|
|
||||||
$this->warn(trans_choice('admin/settings/message.location_scoping.mismatch', count($mismatched)));
|
|
||||||
$this->newLine();
|
|
||||||
$this->info('Edit your locations to associate them with the correct company.');
|
|
||||||
|
|
||||||
$header = ['Type', 'ID', 'Name', 'Checkout Type', 'Company ID', 'Item Company', 'Item Location', 'Location Company', 'Location Company ID'];
|
|
||||||
sort($mismatched);
|
|
||||||
|
|
||||||
$this->table($header, $mismatched);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,7 @@ class Kernel extends ConsoleKernel
|
|||||||
*/
|
*/
|
||||||
protected function schedule(Schedule $schedule)
|
protected function schedule(Schedule $schedule)
|
||||||
{
|
{
|
||||||
if(Setting::getSettings()?->alerts_enabled === 1) {
|
if(Setting::getSettings()->alerts_enabled === 1) {
|
||||||
$schedule->command('snipeit:inventory-alerts')->daily();
|
$schedule->command('snipeit:inventory-alerts')->daily();
|
||||||
$schedule->command('snipeit:expiring-alerts')->daily();
|
$schedule->command('snipeit:expiring-alerts')->daily();
|
||||||
$schedule->command('snipeit:expected-checkin')->daily();
|
$schedule->command('snipeit:expected-checkin')->daily();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class CheckoutableCheckedIn
|
|||||||
$this->checkedOutTo = $checkedOutTo;
|
$this->checkedOutTo = $checkedOutTo;
|
||||||
$this->checkedInBy = $checkedInBy;
|
$this->checkedInBy = $checkedInBy;
|
||||||
$this->note = $note;
|
$this->note = $note;
|
||||||
$this->action_date = $action_date ?? date('Y-m-d H:i:s');
|
$this->action_date = $action_date ?? date('Y-m-d');
|
||||||
$this->originalValues = $originalValues;
|
$this->originalValues = $originalValues;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class AssetNotRequestable extends Exception
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ use Illuminate\Support\Facades\Log;
|
|||||||
use Throwable;
|
use Throwable;
|
||||||
use JsonException;
|
use JsonException;
|
||||||
use Carbon\Exceptions\InvalidFormatException;
|
use Carbon\Exceptions\InvalidFormatException;
|
||||||
use Illuminate\Http\Exceptions\ThrottleRequestsException;
|
|
||||||
|
|
||||||
class Handler extends ExceptionHandler
|
class Handler extends ExceptionHandler
|
||||||
{
|
{
|
||||||
@@ -108,43 +107,25 @@ class Handler extends ExceptionHandler
|
|||||||
|
|
||||||
$statusCode = $e->getStatusCode();
|
$statusCode = $e->getStatusCode();
|
||||||
|
|
||||||
// API throttle requests are handled in the RouteServiceProvider configureRateLimiting() method, so we don't need to handle them here
|
|
||||||
switch ($e->getStatusCode()) {
|
switch ($e->getStatusCode()) {
|
||||||
case '404':
|
case '404':
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode . ' endpoint not found'), 404);
|
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode . ' endpoint not found'), 404);
|
||||||
|
case '429':
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, 'Too many requests'), 429);
|
||||||
case '405':
|
case '405':
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
|
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
|
||||||
default:
|
default:
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode);
|
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 :(
|
// 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
|
// The only alternative is to set that at *each* route, which is crazypants
|
||||||
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
|
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
|
||||||
$ids = method_exists($e, 'getIds') ? $e->getIds() : [];
|
$model_name = last(explode('\\', $e->getModel()));
|
||||||
|
|
||||||
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 = trim(strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel()))))));
|
|
||||||
$route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
|
$route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
|
||||||
|
|
||||||
// Sigh.
|
// Sigh.
|
||||||
@@ -156,12 +137,6 @@ class Handler extends ExceptionHandler
|
|||||||
$route = 'models.index';
|
$route = 'models.index';
|
||||||
} elseif ($route == 'predefinedkits.index') {
|
} elseif ($route == 'predefinedkits.index') {
|
||||||
$route = 'kits.index';
|
$route = 'kits.index';
|
||||||
} elseif ($route == 'assetmaintenances.index') {
|
|
||||||
$route = 'maintenances.index';
|
|
||||||
} elseif ($route === 'licenseseats.index') {
|
|
||||||
$route = 'licenses.index';
|
|
||||||
} elseif (($route === 'customfieldsets.index') || ($route === 'customfields.index')) {
|
|
||||||
$route = 'fields.index';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
@@ -220,9 +195,8 @@ class Handler extends ExceptionHandler
|
|||||||
*/
|
*/
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
|
|
||||||
$this->reportable(function (Throwable $e) {
|
$this->reportable(function (Throwable $e) {
|
||||||
//
|
//
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class ItemStillHasAccessories extends ItemStillHasChildren
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class ItemStillHasAssetModels extends ItemStillHasChildren
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class ItemStillHasAssets extends ItemStillHasChildren
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class ItemStillHasChildren extends Exception
|
|
||||||
{
|
|
||||||
//public function __construct($message, $code = 0, Exception $previous = null, $parent, $children)
|
|
||||||
//{
|
|
||||||
// trans()
|
|
||||||
//
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class ItemStillHasComponents extends ItemStillHasChildren
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class ItemStillHasConsumables extends ItemStillHasChildren
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class ItemStillHasLicenses extends ItemStillHasChildren
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class ItemStillHasMaintenances extends ItemStillHasChildren
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class UserDoestExistException extends Exception
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -12,8 +12,6 @@ use App\Models\Depreciation;
|
|||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\Statuslabel;
|
use App\Models\Statuslabel;
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
use App\Models\Location;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Support\Facades\Crypt;
|
use Illuminate\Support\Facades\Crypt;
|
||||||
use Illuminate\Contracts\Encryption\DecryptException;
|
use Illuminate\Contracts\Encryption\DecryptException;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
@@ -95,7 +93,7 @@ class Helper
|
|||||||
$Parsedown->setSafeMode(true);
|
$Parsedown->setSafeMode(true);
|
||||||
|
|
||||||
if ($str) {
|
if ($str) {
|
||||||
return $Parsedown->text(strip_tags($str));
|
return $Parsedown->text($str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +103,7 @@ class Helper
|
|||||||
$Parsedown->setSafeMode(true);
|
$Parsedown->setSafeMode(true);
|
||||||
|
|
||||||
if ($str) {
|
if ($str) {
|
||||||
return $Parsedown->line(strip_tags($str));
|
return $Parsedown->line($str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,34 +433,6 @@ class Helper
|
|||||||
return $colors[$index];
|
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.
|
* Increases or decreases the brightness of a color by a percentage of the current brightness.
|
||||||
*
|
*
|
||||||
@@ -751,8 +721,8 @@ class Helper
|
|||||||
// The check and message that the user is still using the deprecated version
|
// The check and message that the user is still using the deprecated version
|
||||||
$deprecations = [
|
$deprecations = [
|
||||||
'ms_teams_deprecated' => array(
|
'ms_teams_deprecated' => array(
|
||||||
'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows') && (Setting::getSettings()->webhook_selected === 'microsoft'),
|
'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows'),
|
||||||
'message' => 'The Microsoft Teams webhook URL being used will be deprecated Dec 31st, 2025. <a class="btn btn-primary" href="' . route('settings.slack.index') . '">Change webhook endpoint</a>'),
|
'message' => 'The Microsoft Teams webhook URL being used will be deprecated Jan 31st, 2025. <a class="btn btn-primary" href="' . route('settings.slack.index') . '">Change webhook endpoint</a>'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// if item of concern is being used and its being used with the deprecated values return the notification array.
|
// if item of concern is being used and its being used with the deprecated values return the notification array.
|
||||||
@@ -898,49 +868,7 @@ class Helper
|
|||||||
$filetype = @finfo_file($finfo, $file);
|
$filetype = @finfo_file($finfo, $file);
|
||||||
finfo_close($finfo);
|
finfo_close($finfo);
|
||||||
|
|
||||||
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif') || ($filetype == 'image/avif') || ($filetype == 'image/webp')) {
|
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif') || ($filetype == 'image/avif')) {
|
||||||
return $filetype;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 $filetype;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -967,12 +895,6 @@ class Helper
|
|||||||
public static function selectedPermissionsArray($permissions, $selected_arr = [])
|
public static function selectedPermissionsArray($permissions, $selected_arr = [])
|
||||||
{
|
{
|
||||||
$permissions_arr = [];
|
$permissions_arr = [];
|
||||||
if (is_array($permissions)) {
|
|
||||||
$permissions = json_encode($permissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set default to empty JSON if the value is null
|
|
||||||
$permissions = json_decode($permissions ?? '{}', JSON_OBJECT_AS_ARRAY);
|
|
||||||
|
|
||||||
foreach ($permissions as $permission) {
|
foreach ($permissions as $permission) {
|
||||||
for ($x = 0; $x < count($permission); $x++) {
|
for ($x = 0; $x < count($permission); $x++) {
|
||||||
@@ -983,13 +905,13 @@ class Helper
|
|||||||
if (is_array($selected_arr)) {
|
if (is_array($selected_arr)) {
|
||||||
|
|
||||||
if (array_key_exists($permission_name, $selected_arr)) {
|
if (array_key_exists($permission_name, $selected_arr)) {
|
||||||
$permissions_arr[$permission_name] = (int) $selected_arr[$permission_name];
|
$permissions_arr[$permission_name] = $selected_arr[$permission_name];
|
||||||
} else {
|
} else {
|
||||||
$permissions_arr[$permission_name] = 0;
|
$permissions_arr[$permission_name] = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$permissions_arr[$permission_name] = 0;
|
$permissions_arr[$permission_name] = '0';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1225,42 +1147,22 @@ class Helper
|
|||||||
'webp' => 'far fa-image',
|
'webp' => 'far fa-image',
|
||||||
'avif' => 'far fa-image',
|
'avif' => 'far fa-image',
|
||||||
'svg' => 'fas fa-vector-square',
|
'svg' => 'fas fa-vector-square',
|
||||||
|
|
||||||
// word
|
// word
|
||||||
'doc' => 'far fa-file-word',
|
'doc' => 'far fa-file-word',
|
||||||
'docx' => 'far fa-file-word',
|
'docx' => 'far fa-file-word',
|
||||||
|
|
||||||
// Excel
|
// Excel
|
||||||
'xls' => 'far fa-file-excel',
|
'xls' => 'far fa-file-excel',
|
||||||
'xlsx' => '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
|
// archive
|
||||||
'zip' => 'fas fa-file-archive',
|
'zip' => 'fas fa-file-archive',
|
||||||
'rar' => 'fas fa-file-archive',
|
'rar' => 'fas fa-file-archive',
|
||||||
|
|
||||||
//Text
|
//Text
|
||||||
'odt' => 'far fa-file-alt',
|
|
||||||
'txt' => 'far fa-file-alt',
|
'txt' => 'far fa-file-alt',
|
||||||
'rtf' => 'far fa-file-alt',
|
'rtf' => 'far fa-file-alt',
|
||||||
'xml' => 'fas fa-code',
|
'xml' => 'fas fa-code',
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
'pdf' => 'far fa-file-pdf',
|
'pdf' => 'far fa-file-pdf',
|
||||||
'lic' => 'far fa-save',
|
'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)) {
|
if ($extension && array_key_exists($extension, $allowedExtensionMap)) {
|
||||||
@@ -1404,24 +1306,25 @@ class Helper
|
|||||||
switch ($item) {
|
switch ($item) {
|
||||||
case 'asset':
|
case 'asset':
|
||||||
return 'fas fa-barcode';
|
return 'fas fa-barcode';
|
||||||
|
break;
|
||||||
case 'accessory':
|
case 'accessory':
|
||||||
return 'fas fa-keyboard';
|
return 'fas fa-keyboard';
|
||||||
|
break;
|
||||||
case 'component':
|
case 'component':
|
||||||
return 'fas fa-hdd';
|
return 'fas fa-hdd';
|
||||||
|
break;
|
||||||
case 'consumable':
|
case 'consumable':
|
||||||
return 'fas fa-tint';
|
return 'fas fa-tint';
|
||||||
|
break;
|
||||||
case 'license':
|
case 'license':
|
||||||
return 'far fa-save';
|
return 'far fa-save';
|
||||||
|
break;
|
||||||
case 'location':
|
case 'location':
|
||||||
return 'fas fa-map-marker-alt';
|
return 'fas fa-map-marker-alt';
|
||||||
|
break;
|
||||||
case 'user':
|
case 'user':
|
||||||
return 'fas fa-user';
|
return 'fas fa-user';
|
||||||
case 'supplier':
|
break;
|
||||||
return 'fa-solid fa-store';
|
|
||||||
case 'manufacturer':
|
|
||||||
return 'fa-solid fa-building';
|
|
||||||
case 'category':
|
|
||||||
return 'fa-solid fa-table-columns';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1571,168 +1474,59 @@ class Helper
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static public function getRedirectOption($request, $id, $table, $item_id = null) : RedirectResponse
|
static public function getRedirectOption($request, $id, $table, $item_id = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
$redirect_option = Session::get('redirect_option') ?? $request->redirect_option;
|
$redirect_option = Session::get('redirect_option');
|
||||||
$checkout_to_type = Session::get('checkout_to_type') ?? null;
|
$checkout_to_type = Session::get('checkout_to_type');
|
||||||
$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
|
// return to index
|
||||||
if ($redirect_option == 'index') {
|
if ($redirect_option == 'index') {
|
||||||
return match ($table) {
|
switch ($table) {
|
||||||
'Assets' => redirect()->route('hardware.index'),
|
case "Assets":
|
||||||
'Users' => redirect()->route('users.index'),
|
return route('hardware.index');
|
||||||
'Licenses' => redirect()->route('licenses.index'),
|
case "Users":
|
||||||
'Accessories' => redirect()->route('accessories.index'),
|
return route('users.index');
|
||||||
'Components' => redirect()->route('components.index'),
|
case "Licenses":
|
||||||
'Consumables' => redirect()->route('consumables.index'),
|
return route('licenses.index');
|
||||||
};
|
case "Accessories":
|
||||||
|
return route('accessories.index');
|
||||||
|
case "Components":
|
||||||
|
return route('components.index');
|
||||||
|
case "Consumables":
|
||||||
|
return route('consumables.index');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return to thing being assigned
|
// return to thing being assigned
|
||||||
if ($redirect_option == 'item') {
|
if ($redirect_option == 'item') {
|
||||||
return match ($table) {
|
switch ($table) {
|
||||||
'Assets' => redirect()->route('hardware.show', $id ?? $item_id),
|
case "Assets":
|
||||||
'Users' => redirect()->route('users.show', $id ?? $item_id),
|
return route('hardware.show', $id ?? $item_id);
|
||||||
'Licenses' => redirect()->route('licenses.show', $id ?? $item_id),
|
case "Users":
|
||||||
'Accessories' => redirect()->route('accessories.show', $id ?? $item_id),
|
return route('users.show', $id ?? $item_id);
|
||||||
'Components' => redirect()->route('components.show', $id ?? $item_id),
|
case "Licenses":
|
||||||
'Consumables' => redirect()->route('consumables.show', $id ?? $item_id),
|
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 to assignment target
|
// return to assignment target
|
||||||
if ($redirect_option == 'target') {
|
if ($redirect_option == 'target') {
|
||||||
return match ($checkout_to_type) {
|
switch ($checkout_to_type) {
|
||||||
'user' => redirect()->route('users.show', $request->assigned_user ?? $checkedInFrom),
|
case 'user':
|
||||||
'location' => redirect()->route('locations.show', $request->assigned_location ?? $checkedInFrom),
|
return route('users.show', $request->assigned_user);
|
||||||
'asset' => redirect()->route('hardware.show', $request->assigned_asset ?? $checkedInFrom),
|
case 'location':
|
||||||
};
|
return route('locations.show', $request->assigned_location);
|
||||||
|
case 'asset':
|
||||||
|
return route('hardware.show', $request->assigned_asset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return to somewhere else
|
|
||||||
if ($redirect_option == 'other_redirect') {
|
|
||||||
return match ($other_redirect) {
|
|
||||||
'audit' => redirect()->route('assets.audit.due'),
|
|
||||||
'model' => redirect()->route('models.show', $request->model_id),
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'));
|
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check for inconsistencies before activating scoped locations with FullMultipleCompanySupport
|
|
||||||
* If there are locations with different companies than related objects unforseen problems could arise
|
|
||||||
*
|
|
||||||
* @author T. Regnery <tobias.regnery@gmail.com>
|
|
||||||
* @since 7.0
|
|
||||||
*
|
|
||||||
* @param $artisan when false, bail out on first inconsistent entry
|
|
||||||
* @param $location_id when set, only test this specific location
|
|
||||||
* @param $new_company_id in case of updating a location, this is the newly requested company_id
|
|
||||||
* @return string []
|
|
||||||
*/
|
|
||||||
static public function test_locations_fmcs($artisan, $location_id = null, $new_company_id = null) {
|
|
||||||
$mismatched = [];
|
|
||||||
|
|
||||||
if ($location_id) {
|
|
||||||
$location = Location::find($location_id);
|
|
||||||
if ($location) {
|
|
||||||
$locations = collect([])->push(Location::find($location_id));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$locations = Location::all();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bail out early if there are no locations
|
|
||||||
if ($locations->count() == 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach($locations as $location) {
|
|
||||||
// in case of an update of a single location, use the newly requested company_id
|
|
||||||
if ($new_company_id) {
|
|
||||||
$location_company = $new_company_id;
|
|
||||||
} else {
|
|
||||||
$location_company = $location->company_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Depending on the relationship, we must use different operations to retrieve the objects
|
|
||||||
$keywords_relation = [
|
|
||||||
'many' => [
|
|
||||||
'accessories',
|
|
||||||
'assets',
|
|
||||||
'assignedAccessories',
|
|
||||||
'assignedAssets',
|
|
||||||
'components',
|
|
||||||
'consumables',
|
|
||||||
'rtd_assets',
|
|
||||||
'users',
|
|
||||||
],
|
|
||||||
'one' => [
|
|
||||||
'manager',
|
|
||||||
'parent',
|
|
||||||
]];
|
|
||||||
|
|
||||||
// In case of a single location, the children must be checked as well, because we don't walk every location
|
|
||||||
if ($location_id) {
|
|
||||||
$keywords_relation['many'][] = 'children';
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($keywords_relation as $relation => $keywords) {
|
|
||||||
foreach($keywords as $keyword) {
|
|
||||||
if ($relation == 'many') {
|
|
||||||
$items = $location->{$keyword}->all();
|
|
||||||
} else {
|
|
||||||
$items = collect([])->push($location->$keyword);
|
|
||||||
}
|
|
||||||
|
|
||||||
$count = 0;
|
|
||||||
foreach ($items as $item) {
|
|
||||||
|
|
||||||
|
|
||||||
if ($item && $item->company_id != $location_company) {
|
|
||||||
|
|
||||||
$mismatched[] = [
|
|
||||||
class_basename(get_class($item)),
|
|
||||||
$item->id,
|
|
||||||
$item->name ?? $item->asset_tag ?? $item->serial ?? $item->username,
|
|
||||||
$item->assigned_type ? str_replace('App\\Models\\', '', $item->assigned_type) : null,
|
|
||||||
$item->company_id ?? null,
|
|
||||||
$item->company->name ?? null,
|
|
||||||
// $item->defaultLoc->id ?? null,
|
|
||||||
// $item->defaultLoc->name ?? null,
|
|
||||||
// $item->defaultLoc->company->id ?? null,
|
|
||||||
// $item->defaultLoc->company->name ?? null,
|
|
||||||
$item->location->name ?? null,
|
|
||||||
$item->location->company->name ?? null,
|
|
||||||
$location_company ?? null,
|
|
||||||
];
|
|
||||||
|
|
||||||
$count++;
|
|
||||||
|
|
||||||
// Bail early if this is not being run via artisan
|
|
||||||
if ((!$artisan) && ($count > 0)) {
|
|
||||||
return $mismatched;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $mismatched;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class IconHelper
|
|||||||
case 'clone':
|
case 'clone':
|
||||||
return 'far fa-clone';
|
return 'far fa-clone';
|
||||||
case 'delete':
|
case 'delete':
|
||||||
case 'upload deleted':
|
|
||||||
return 'fas fa-trash';
|
return 'fas fa-trash';
|
||||||
case 'create':
|
case 'create':
|
||||||
return 'fa-solid fa-plus';
|
return 'fa-solid fa-plus';
|
||||||
@@ -40,14 +39,10 @@ class IconHelper
|
|||||||
return 'fa-solid fa-trash-arrow-up';
|
return 'fa-solid fa-trash-arrow-up';
|
||||||
case 'external-link':
|
case 'external-link':
|
||||||
return 'fa fa-external-link';
|
return 'fa fa-external-link';
|
||||||
case 'link':
|
|
||||||
return 'fa fa-link';
|
|
||||||
case 'email':
|
case 'email':
|
||||||
return 'fa-regular fa-envelope';
|
return 'fa-regular fa-envelope';
|
||||||
case 'phone':
|
case 'phone':
|
||||||
return 'fa-solid fa-phone';
|
return 'fa-solid fa-phone';
|
||||||
case 'mobile':
|
|
||||||
return 'fas fa-mobile-screen-button';
|
|
||||||
case 'long-arrow-right':
|
case 'long-arrow-right':
|
||||||
return 'fas fa-long-arrow-alt-right';
|
return 'fas fa-long-arrow-alt-right';
|
||||||
case 'download':
|
case 'download':
|
||||||
@@ -142,7 +137,7 @@ class IconHelper
|
|||||||
case 'more-files':
|
case 'more-files':
|
||||||
return 'fa-solid fa-laptop-file';
|
return 'fa-solid fa-laptop-file';
|
||||||
case 'maintenances':
|
case 'maintenances':
|
||||||
return 'fa-solid fa-screwdriver-wrench';
|
return 'fas fa-wrench';
|
||||||
case 'seats':
|
case 'seats':
|
||||||
return 'far fa-list-alt';
|
return 'far fa-list-alt';
|
||||||
case 'globe-us':
|
case 'globe-us':
|
||||||
@@ -156,7 +151,6 @@ class IconHelper
|
|||||||
case 'location':
|
case 'location':
|
||||||
return 'fas fa-map-marker-alt';
|
return 'fas fa-map-marker-alt';
|
||||||
case 'superadmin':
|
case 'superadmin':
|
||||||
case 'admin':
|
|
||||||
return 'fas fa-crown';
|
return 'fas fa-crown';
|
||||||
case 'print':
|
case 'print':
|
||||||
return 'fa-solid fa-print';
|
return 'fa-solid fa-print';
|
||||||
@@ -197,10 +191,6 @@ class IconHelper
|
|||||||
case 'note':
|
case 'note':
|
||||||
case 'notes':
|
case 'notes':
|
||||||
return 'fas fa-sticky-note';
|
return 'fas fa-sticky-note';
|
||||||
case 'tip':
|
|
||||||
return 'fa-solid fa-lightbulb';
|
|
||||||
case 'highlight':
|
|
||||||
return 'fa-solid fa-highlighter';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,84 +16,38 @@ class StorageHelper
|
|||||||
$disk = config('filesystems.default');
|
$disk = config('filesystems.default');
|
||||||
}
|
}
|
||||||
switch (config("filesystems.disks.$disk.driver")) {
|
switch (config("filesystems.disks.$disk.driver")) {
|
||||||
case 'local':
|
case 'local':
|
||||||
return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
|
return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
|
||||||
|
|
||||||
case 's3':
|
case 's3':
|
||||||
return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
|
return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return Storage::disk($disk)->download($filename);
|
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
|
* This determines the file types that should be allowed inline and checks their fileinfo extension
|
||||||
* to determine that they are safe to display inline.
|
* to determine that they are safe to display inline.
|
||||||
*
|
*
|
||||||
* @author <A. Gianotto> [<snipe@snipe.net]>
|
* @author <A. Gianotto> [<snipe@snipe.net]>
|
||||||
* @since v7.0.14
|
* @since v7.0.14
|
||||||
* @param $file_with_path
|
* @param $file_with_path
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function allowSafeInline($file_with_path)
|
public static function allowSafeInline($file_with_path) {
|
||||||
{
|
|
||||||
|
|
||||||
$allowed_inline = [
|
$allowed_inline = [
|
||||||
'avif',
|
|
||||||
'gif',
|
|
||||||
'gif',
|
|
||||||
'jpg',
|
|
||||||
'mov',
|
|
||||||
'mp3',
|
|
||||||
'mp4',
|
|
||||||
'ogg',
|
|
||||||
'pdf',
|
'pdf',
|
||||||
'png',
|
|
||||||
'svg',
|
'svg',
|
||||||
'wav',
|
'jpg',
|
||||||
'webm',
|
'gif',
|
||||||
|
'svg',
|
||||||
|
'avif',
|
||||||
'webp',
|
'webp',
|
||||||
|
'png',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
@@ -105,24 +59,10 @@ 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.
|
* Decide whether to show the file inline or download it.
|
||||||
*/
|
*/
|
||||||
public static function showOrDownloadFile($file, $filename)
|
public static function showOrDownloadFile($file, $filename) {
|
||||||
{
|
|
||||||
|
|
||||||
$headers = [];
|
$headers = [];
|
||||||
|
|
||||||
|
|||||||
@@ -77,30 +77,13 @@ class AccessoriesController extends Controller
|
|||||||
$accessory->supplier_id = request('supplier_id');
|
$accessory->supplier_id = request('supplier_id');
|
||||||
$accessory->notes = request('notes');
|
$accessory->notes = request('notes');
|
||||||
|
|
||||||
if ($request->has('use_cloned_image')) {
|
$accessory = $request->handleImages($accessory);
|
||||||
$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?
|
// Was the accessory created?
|
||||||
if ($accessory->save()) {
|
if ($accessory->save()) {
|
||||||
// Redirect to the new accessory page
|
// Redirect to the new accessory page
|
||||||
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
|
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.create.success'));
|
||||||
->with('success', trans('admin/accessories/message.create.success'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->back()->withInput()->withErrors($accessory->getErrors());
|
return redirect()->back()->withInput()->withErrors($accessory->getErrors());
|
||||||
@@ -129,14 +112,13 @@ class AccessoriesController extends Controller
|
|||||||
{
|
{
|
||||||
|
|
||||||
$this->authorize('create', Accessory::class);
|
$this->authorize('create', Accessory::class);
|
||||||
$cloned = clone $accessory;
|
|
||||||
$accessory_to_clone = $accessory;
|
$accessory = clone $accessory;
|
||||||
$cloned->id = null;
|
$accessory->id = null;
|
||||||
$cloned->deleted_at = '';
|
$accessory->location_id = null;
|
||||||
|
|
||||||
return view('accessories/edit')
|
return view('accessories/edit')
|
||||||
->with('cloned_model', $accessory_to_clone)
|
->with('item', $accessory);
|
||||||
->with('item', $cloned);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,8 +167,7 @@ class AccessoriesController extends Controller
|
|||||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||||
|
|
||||||
if ($accessory->save()) {
|
if ($accessory->save()) {
|
||||||
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
|
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.update.success'));
|
||||||
->with('success', trans('admin/accessories/message.update.success'));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
|
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
|
||||||
@@ -203,15 +184,15 @@ class AccessoriesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy($accessoryId) : RedirectResponse
|
public function destroy($accessoryId) : RedirectResponse
|
||||||
{
|
{
|
||||||
if (is_null($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryId))) {
|
if (is_null($accessory = Accessory::find($accessoryId))) {
|
||||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
|
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->authorize($accessory);
|
$this->authorize($accessory);
|
||||||
|
|
||||||
|
|
||||||
if ($accessory->checkouts_count > 0) {
|
if ($accessory->hasUsers() > 0) {
|
||||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/general.delete_disabled'));
|
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.assoc_users', ['count'=> $accessory->hasUsers()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($accessory->image) {
|
if ($accessory->image) {
|
||||||
@@ -239,10 +220,7 @@ class AccessoriesController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function show(Accessory $accessory) : View | RedirectResponse
|
public function show(Accessory $accessory) : View | RedirectResponse
|
||||||
{
|
{
|
||||||
$accessory->loadCount('checkouts as checkouts_count');
|
$accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessory->id);
|
||||||
|
|
||||||
$accessory->load(['adminuser' => fn($query) => $query->withTrashed()]);
|
|
||||||
|
|
||||||
$this->authorize('view', $accessory);
|
$this->authorize('view', $accessory);
|
||||||
return view('accessories.view', compact('accessory'));
|
return view('accessories.view', compact('accessory'));
|
||||||
}
|
}
|
||||||
|
|||||||
132
app/Http/Controllers/Accessories/AccessoriesFilesController.php
Normal file
132
app/Http/Controllers/Accessories/AccessoriesFilesController.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<?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'));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use App\Helpers\Helper;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Accessory;
|
use App\Models\Accessory;
|
||||||
use App\Models\AccessoryCheckout;
|
use App\Models\AccessoryCheckout;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use \Illuminate\Contracts\View\View;
|
use \Illuminate\Contracts\View\View;
|
||||||
@@ -29,17 +30,9 @@ class AccessoryCheckinController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$accessory = Accessory::find($accessory_user->accessory_id);
|
$accessory = Accessory::find($accessory_user->accessory_id);
|
||||||
|
|
||||||
//based on what the accessory is checked out to the target redirect option will be displayed accordingly.
|
|
||||||
$target_option = match ($accessory_user->assigned_type) {
|
|
||||||
'App\Models\Asset' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.asset')]),
|
|
||||||
'App\Models\Location' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.location')]),
|
|
||||||
default => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.user')]),
|
|
||||||
};
|
|
||||||
$this->authorize('checkin', $accessory);
|
$this->authorize('checkin', $accessory);
|
||||||
|
|
||||||
return view('accessories/checkin', compact('accessory', 'target_option'))->with('backto', $backto);
|
return view('accessories/checkin', compact('accessory'))->with('backto', $backto);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,14 +51,8 @@ class AccessoryCheckinController extends Controller
|
|||||||
|
|
||||||
$accessory = Accessory::find($accessory_checkout->accessory_id);
|
$accessory = Accessory::find($accessory_checkout->accessory_id);
|
||||||
|
|
||||||
session()->put('checkedInFrom', $accessory_checkout->assigned_to);
|
|
||||||
session()->put('checkout_to_type', match ($accessory_checkout->assigned_type) {
|
|
||||||
'App\Models\User' => 'user',
|
|
||||||
'App\Models\Location' => 'location',
|
|
||||||
'App\Models\Asset' => 'asset',
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->authorize('checkin', $accessory);
|
$this->authorize('checkin', $accessory);
|
||||||
|
|
||||||
$checkin_hours = date('H:i:s');
|
$checkin_hours = date('H:i:s');
|
||||||
$checkin_at = date('Y-m-d H:i:s');
|
$checkin_at = date('Y-m-d H:i:s');
|
||||||
if ($request->filled('checkin_at')) {
|
if ($request->filled('checkin_at')) {
|
||||||
@@ -78,8 +65,7 @@ class AccessoryCheckinController extends Controller
|
|||||||
|
|
||||||
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
session()->put(['redirect_option' => $request->get('redirect_option')]);
|
||||||
|
|
||||||
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
|
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.checkin.success'));
|
||||||
->with('success', trans('admin/accessories/message.checkin.success'));
|
|
||||||
}
|
}
|
||||||
// Redirect to the accessory management page with error
|
// Redirect to the accessory management page with error
|
||||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkin.error'));
|
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkin.error'));
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ class AccessoryCheckoutController extends Controller
|
|||||||
$this->authorize('checkout', $accessory);
|
$this->authorize('checkout', $accessory);
|
||||||
|
|
||||||
$target = $this->determineCheckoutTarget();
|
$target = $this->determineCheckoutTarget();
|
||||||
session()->put(['checkout_to_type' => $target]);
|
|
||||||
|
|
||||||
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
$accessory->checkout_qty = $request->input('checkout_qty', 1);
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ class AccessoryCheckoutController extends Controller
|
|||||||
|
|
||||||
|
|
||||||
// Redirect to the new accessory page
|
// Redirect to the new accessory page
|
||||||
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
|
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))
|
||||||
->with('success', trans('admin/accessories/message.checkout.success'));
|
->with('success', trans('admin/accessories/message.checkout.success'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,24 +7,30 @@ use App\Events\CheckoutDeclined;
|
|||||||
use App\Events\ItemAccepted;
|
use App\Events\ItemAccepted;
|
||||||
use App\Events\ItemDeclined;
|
use App\Events\ItemDeclined;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Mail\CheckoutAcceptanceResponseMail;
|
use App\Models\Actionlog;
|
||||||
|
use App\Models\Asset;
|
||||||
use App\Models\CheckoutAcceptance;
|
use App\Models\CheckoutAcceptance;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\Contracts\Acceptable;
|
use App\Models\Contracts\Acceptable;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Models\User;
|
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\AcceptanceAssetAcceptedNotification;
|
||||||
use App\Notifications\AcceptanceAssetAcceptedToUserNotification;
|
|
||||||
use App\Notifications\AcceptanceAssetDeclinedNotification;
|
use App\Notifications\AcceptanceAssetDeclinedNotification;
|
||||||
use Exception;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use App\Http\Controllers\SettingsController;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Carbon\Carbon;
|
||||||
use \Illuminate\Contracts\View\View;
|
use \Illuminate\Contracts\View\View;
|
||||||
use \Illuminate\Http\RedirectResponse;
|
use \Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use App\Helpers\Helper;
|
|
||||||
|
|
||||||
class AcceptanceController extends Controller
|
class AcceptanceController extends Controller
|
||||||
{
|
{
|
||||||
@@ -75,10 +81,6 @@ class AcceptanceController extends Controller
|
|||||||
public function store(Request $request, $id) : RedirectResponse
|
public function store(Request $request, $id) : RedirectResponse
|
||||||
{
|
{
|
||||||
$acceptance = CheckoutAcceptance::find($id);
|
$acceptance = CheckoutAcceptance::find($id);
|
||||||
$assigned_user = User::find($acceptance->assigned_to_id);
|
|
||||||
$settings = Setting::getSettings();
|
|
||||||
$sig_filename='';
|
|
||||||
|
|
||||||
|
|
||||||
if (is_null($acceptance)) {
|
if (is_null($acceptance)) {
|
||||||
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
|
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||||
@@ -101,95 +103,142 @@ class AcceptanceController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for the signature directory
|
* Get the signature and save it
|
||||||
*/
|
*/
|
||||||
if (! Storage::exists('private_uploads/signatures')) {
|
if (! Storage::exists('private_uploads/signatures')) {
|
||||||
Storage::makeDirectory('private_uploads/signatures', 775);
|
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);
|
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
|
||||||
|
$display_model = '';
|
||||||
// If signatures are required, make sure we have one
|
$pdf_view_route = '';
|
||||||
if (Setting::getSettings()->require_accept_signature == '1') {
|
$pdf_filename = 'accepted-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
||||||
|
$sig_filename='';
|
||||||
// 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') {
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate the PDF content
|
// The item was accepted, check for a signature
|
||||||
$pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
|
if ($request->filled('signature_output')) {
|
||||||
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf_content);
|
$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);
|
||||||
|
|
||||||
// Log the acceptance
|
// No image data is present, kick them back.
|
||||||
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
|
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
|
||||||
|
} else {
|
||||||
// Send the PDF to the signing user
|
return redirect()->back()->with('error', trans('general.shitty_browser'));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
if (is_null($branding_settings->logo)){
|
||||||
|
$path_logo = "";
|
||||||
|
} else {
|
||||||
|
$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());
|
||||||
|
}
|
||||||
|
|
||||||
|
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
|
||||||
try {
|
try {
|
||||||
$acceptance->notify((new AcceptanceAssetAcceptedNotification($data))->locale(Setting::getSettings()->locale));
|
$acceptance->notify(new AcceptanceAssetAcceptedNotification($data));
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::warning($e);
|
Log::warning($e);
|
||||||
}
|
}
|
||||||
@@ -197,43 +246,97 @@ class AcceptanceController extends Controller
|
|||||||
|
|
||||||
$return_msg = trans('admin/users/message.accepted');
|
$return_msg = trans('admin/users/message.accepted');
|
||||||
|
|
||||||
// Item was declined
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
for ($i = 0; $i < ($acceptance->qty ?? 1); $i++) {
|
/**
|
||||||
$acceptance->decline($sig_filename, $request->input('note'));
|
* 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') {
|
||||||
|
|
||||||
|
// 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));
|
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
|
||||||
Log::debug('New event acceptance.');
|
|
||||||
event(new CheckoutDeclined($acceptance));
|
event(new CheckoutDeclined($acceptance));
|
||||||
$return_msg = trans('admin/users/message.declined');
|
$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);
|
return redirect()->to('account/accept')->with('success', $return_msg);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,11 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Models\Actionlog;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
||||||
use \Illuminate\Http\Response;
|
use \Illuminate\Http\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
class ActionlogController extends Controller
|
class ActionlogController extends Controller
|
||||||
{
|
{
|
||||||
public function displaySig($filename) : RedirectResponse | Response | bool
|
public function displaySig($filename) : RedirectResponse | Response | bool
|
||||||
@@ -41,29 +39,17 @@ class ActionlogController extends Controller
|
|||||||
|
|
||||||
public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
|
public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
|
||||||
{
|
{
|
||||||
|
$this->authorize('view', \App\Models\Asset::class);
|
||||||
|
|
||||||
if ($actionlog = Actionlog::where('filename', $filename)->with('user')->with('target')->firstOrFail()) {
|
if (config('filesystems.default') == 's3_private') {
|
||||||
|
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/'.$filename, now()->addMinutes(5)));
|
||||||
$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'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->back()->with('error', trans('general.record_not_found'));
|
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'));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,15 +54,6 @@ class AccessoriesController extends Controller
|
|||||||
'notes',
|
'notes',
|
||||||
'checkouts_count',
|
'checkouts_count',
|
||||||
'qty',
|
'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',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
@@ -70,25 +61,12 @@ class AccessoriesController extends Controller
|
|||||||
->with('category', 'company', 'manufacturer', 'checkouts', 'location', 'supplier', 'adminuser')
|
->with('category', 'company', 'manufacturer', 'checkouts', 'location', 'supplier', 'adminuser')
|
||||||
->withCount('checkouts as checkouts_count');
|
->withCount('checkouts as checkouts_count');
|
||||||
|
|
||||||
$filter = [];
|
if ($request->filled('search')) {
|
||||||
|
$accessories = $accessories->TextSearch($request->input('search'));
|
||||||
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')) {
|
if ($request->filled('company_id')) {
|
||||||
$accessories->where('accessories.company_id', '=', $request->input('company_id'));
|
$accessories->where('company_id', '=', $request->input('company_id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('category_id')) {
|
if ($request->filled('category_id')) {
|
||||||
@@ -271,11 +249,11 @@ class AccessoriesController extends Controller
|
|||||||
public function destroy($id)
|
public function destroy($id)
|
||||||
{
|
{
|
||||||
$this->authorize('delete', Accessory::class);
|
$this->authorize('delete', Accessory::class);
|
||||||
$accessory = Accessory::withCount('checkouts as checkouts_count')->findOrFail($id);
|
$accessory = Accessory::findOrFail($id);
|
||||||
$this->authorize($accessory);
|
$this->authorize($accessory);
|
||||||
|
|
||||||
if ($accessory->checkouts_count > 0) {
|
if ($accessory->hasUsers() > 0) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/general.delete_disabled')));
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.assoc_users', ['count'=> $accessory->hasUsers()])));
|
||||||
}
|
}
|
||||||
|
|
||||||
$accessory->delete();
|
$accessory->delete();
|
||||||
@@ -310,42 +288,32 @@ class AccessoriesController extends Controller
|
|||||||
'note' => $request->input('note'),
|
'note' => $request->input('note'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
$accessory_checkout->created_by = auth()->id();
|
$accessory_checkout->created_by = auth()->id();
|
||||||
$accessory_checkout->save();
|
$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
|
// Set this value to be able to pass the qty through to the event
|
||||||
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
|
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check in the item so that it can be checked out again to someone else
|
* 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 Request $request
|
||||||
* @param int $accessoryUserId
|
* @param int $accessoryUserId
|
||||||
* @param string $backto
|
* @param string $backto
|
||||||
* @return JsonResponse
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
* @uses Accessory::checkin_email() to determine if an email can and should be sent
|
|
||||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
|
||||||
* @internal param int $accessoryId
|
* @internal param int $accessoryId
|
||||||
*/
|
*/
|
||||||
public function checkin(Request $request, $accessoryUserId = null)
|
public function checkin(Request $request, $accessoryUserId = null)
|
||||||
{
|
{
|
||||||
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) {
|
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryUserId))) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist', ['id' => $accessoryUserId])));
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
|
||||||
}
|
}
|
||||||
|
|
||||||
$accessory = Accessory::find($accessory_checkout->accessory_id);
|
$accessory = Accessory::find($accessory_checkout->accessory_id);
|
||||||
@@ -359,14 +327,7 @@ class AccessoriesController extends Controller
|
|||||||
$user = User::find($accessory_checkout->assigned_to);
|
$user = User::find($accessory_checkout->assigned_to);
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload = [
|
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkin.success')));
|
||||||
'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')));
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.checkin.error')));
|
||||||
|
|||||||
200
app/Http/Controllers/Api/AssetFilesController.php
Normal file
200
app/Http/Controllers/Api/AssetFilesController.php
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<?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\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;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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($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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the asset is valid
|
||||||
|
if (isset($asset->id)) {
|
||||||
|
$this->authorize('view', $asset);
|
||||||
|
|
||||||
|
// Check that there are some uploads on this asset that can be listed
|
||||||
|
if ($asset->uploads->count() > 0) {
|
||||||
|
$files = array();
|
||||||
|
foreach ($asset->uploads as $upload) {
|
||||||
|
array_push($files, $upload);
|
||||||
|
}
|
||||||
|
// Give the list of files back to the user
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', $files, trans('admin/hardware/message.upload.success')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are no files.
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', array(), trans('admin/hardware/message.upload.success')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send back an error message
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error')), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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($assetId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
|
||||||
|
{
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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($assetId = null, $fileId = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,11 @@ namespace App\Http\Controllers\Api;
|
|||||||
|
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\ImageUploadRequest;
|
use App\Http\Transformers\AssetMaintenancesTransformer;
|
||||||
use App\Http\Transformers\MaintenancesTransformer;
|
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\Maintenance;
|
use App\Models\AssetMaintenance;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
@@ -18,13 +18,13 @@ use Illuminate\Http\JsonResponse;
|
|||||||
*
|
*
|
||||||
* @version v2.0
|
* @version v2.0
|
||||||
*/
|
*/
|
||||||
class MaintenancesController extends Controller
|
class AssetMaintenancesController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the JSON response for asset maintenances listing view.
|
* Generates the JSON response for asset maintenances listing view.
|
||||||
*
|
*
|
||||||
* @see MaintenancesController::getIndex() method that generates view
|
* @see AssetMaintenancesController::getIndex() method that generates view
|
||||||
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||||
* @version v1.0
|
* @version v1.0
|
||||||
* @since [v1.8]
|
* @since [v1.8]
|
||||||
@@ -33,7 +33,7 @@ class MaintenancesController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('view', Asset::class);
|
$this->authorize('view', Asset::class);
|
||||||
|
|
||||||
$maintenances = Maintenance::select('maintenances.*')
|
$maintenances = AssetMaintenance::select('asset_maintenances.*')
|
||||||
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'adminuser');
|
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'adminuser');
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
@@ -45,15 +45,11 @@ class MaintenancesController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('supplier_id')) {
|
if ($request->filled('supplier_id')) {
|
||||||
$maintenances->where('maintenances.supplier_id', '=', $request->input('supplier_id'));
|
$maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('created_by')) {
|
if ($request->filled('created_by')) {
|
||||||
$maintenances->where('maintenances.created_by', '=', $request->input('created_by'));
|
$maintenances->where('asset_maintenances.created_by', '=', $request->input('created_by'));
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->filled('url')) {
|
|
||||||
$maintenances->where('maintenances.url', '=', $request->input('url'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('asset_maintenance_type')) {
|
if ($request->filled('asset_maintenance_type')) {
|
||||||
@@ -67,7 +63,7 @@ class MaintenancesController extends Controller
|
|||||||
|
|
||||||
$allowed_columns = [
|
$allowed_columns = [
|
||||||
'id',
|
'id',
|
||||||
'name',
|
'title',
|
||||||
'asset_maintenance_time',
|
'asset_maintenance_time',
|
||||||
'asset_maintenance_type',
|
'asset_maintenance_type',
|
||||||
'cost',
|
'cost',
|
||||||
@@ -79,7 +75,6 @@ class MaintenancesController extends Controller
|
|||||||
'serial',
|
'serial',
|
||||||
'created_by',
|
'created_by',
|
||||||
'supplier',
|
'supplier',
|
||||||
'location',
|
|
||||||
'is_warranty',
|
'is_warranty',
|
||||||
'status_label',
|
'status_label',
|
||||||
];
|
];
|
||||||
@@ -103,9 +98,6 @@ class MaintenancesController extends Controller
|
|||||||
case 'serial':
|
case 'serial':
|
||||||
$maintenances = $maintenances->OrderByAssetSerial($order);
|
$maintenances = $maintenances->OrderByAssetSerial($order);
|
||||||
break;
|
break;
|
||||||
case 'location':
|
|
||||||
$maintenances = $maintenances->OrderLocationName($order);
|
|
||||||
break;
|
|
||||||
case 'status_label':
|
case 'status_label':
|
||||||
$maintenances = $maintenances->OrderStatusName($order);
|
$maintenances = $maintenances->OrderStatusName($order);
|
||||||
break;
|
break;
|
||||||
@@ -116,7 +108,7 @@ class MaintenancesController extends Controller
|
|||||||
|
|
||||||
$total = $maintenances->count();
|
$total = $maintenances->count();
|
||||||
$maintenances = $maintenances->skip($offset)->take($limit)->get();
|
$maintenances = $maintenances->skip($offset)->take($limit)->get();
|
||||||
return (new MaintenancesTransformer())->transformMaintenances($maintenances, $total);
|
return (new AssetMaintenancesTransformer())->transformAssetMaintenances($maintenances, $total);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -125,23 +117,22 @@ class MaintenancesController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Validates and stores the new asset maintenance
|
* Validates and stores the new asset maintenance
|
||||||
*
|
*
|
||||||
* @see MaintenancesController::getCreate() method for the form
|
* @see AssetMaintenancesController::getCreate() method for the form
|
||||||
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||||
* @version v1.0
|
* @version v1.0
|
||||||
* @since [v1.8]
|
* @since [v1.8]
|
||||||
*/
|
*/
|
||||||
public function store(ImageUploadRequest $request) : JsonResponse | array
|
public function store(Request $request) : JsonResponse | array
|
||||||
{
|
{
|
||||||
$this->authorize('update', Asset::class);
|
$this->authorize('update', Asset::class);
|
||||||
|
|
||||||
// create a new model instance
|
// create a new model instance
|
||||||
$maintenance = new Maintenance();
|
$maintenance = new AssetMaintenance();
|
||||||
$maintenance->fill($request->all());
|
$maintenance->fill($request->all());
|
||||||
$maintenance->created_by = auth()->id();
|
$maintenance->created_by = auth()->id();
|
||||||
$maintenance = $request->handleImages($maintenance);
|
|
||||||
// Was the asset maintenance created?
|
// Was the asset maintenance created?
|
||||||
if ($maintenance->save()) {
|
if ($maintenance->save()) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.create.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.create.success')));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,11 +153,11 @@ class MaintenancesController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('update', Asset::class);
|
$this->authorize('update', Asset::class);
|
||||||
|
|
||||||
if ($maintenance = Maintenance::with('asset')->find($id)) {
|
if ($maintenance = AssetMaintenance::with('asset')->find($id)) {
|
||||||
|
|
||||||
// Can this user manage this asset?
|
// Can this user manage this asset?
|
||||||
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
||||||
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')])));
|
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')])));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The asset this miantenance is attached to is not valid or has been deleted
|
// The asset this miantenance is attached to is not valid or has been deleted
|
||||||
@@ -177,13 +168,13 @@ class MaintenancesController extends Controller
|
|||||||
$maintenance->fill($request->all());
|
$maintenance->fill($request->all());
|
||||||
|
|
||||||
if ($maintenance->save()) {
|
if ($maintenance->save()) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.edit.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.edit.success')));
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
|
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/maintenances/general.maintenance'), 'id' => $id])));
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id])));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,20 +182,20 @@ class MaintenancesController extends Controller
|
|||||||
* Delete an asset maintenance
|
* Delete an asset maintenance
|
||||||
*
|
*
|
||||||
* @author A. Gianotto <snipe@snipe.net>
|
* @author A. Gianotto <snipe@snipe.net>
|
||||||
* @param int $maintenanceId
|
* @param int $assetMaintenanceId
|
||||||
* @version v1.0
|
* @version v1.0
|
||||||
* @since [v4.0]
|
* @since [v4.0]
|
||||||
*/
|
*/
|
||||||
public function destroy($maintenanceId) : JsonResponse | array
|
public function destroy($assetMaintenanceId) : JsonResponse | array
|
||||||
{
|
{
|
||||||
$this->authorize('update', Asset::class);
|
$this->authorize('update', Asset::class);
|
||||||
// Check if the asset maintenance exists
|
// Check if the asset maintenance exists
|
||||||
|
|
||||||
$maintenance = Maintenance::findOrFail($maintenanceId);
|
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
|
||||||
|
|
||||||
$maintenance->delete();
|
$assetMaintenance->delete();
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.delete.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.delete.success')));
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -213,19 +204,19 @@ class MaintenancesController extends Controller
|
|||||||
* View an asset maintenance
|
* View an asset maintenance
|
||||||
*
|
*
|
||||||
* @author A. Gianotto <snipe@snipe.net>
|
* @author A. Gianotto <snipe@snipe.net>
|
||||||
* @param int $maintenanceId
|
* @param int $assetMaintenanceId
|
||||||
* @version v1.0
|
* @version v1.0
|
||||||
* @since [v4.0]
|
* @since [v4.0]
|
||||||
*/
|
*/
|
||||||
public function show($maintenanceId) : JsonResponse | array
|
public function show($assetMaintenanceId) : JsonResponse | array
|
||||||
{
|
{
|
||||||
$this->authorize('view', Asset::class);
|
$this->authorize('view', Asset::class);
|
||||||
$maintenance = Maintenance::findOrFail($maintenanceId);
|
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
|
||||||
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
if (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot view a maintenance for that asset'));
|
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot view a maintenance for that asset'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (new MaintenancesTransformer())->transformMaintenance($maintenance);
|
return (new AssetMaintenancesTransformer())->transformAssetMaintenance($assetMaintenance);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
179
app/Http/Controllers/Api/AssetModelFilesController.php
Normal file
179
app/Http/Controllers/Api/AssetModelFilesController.php
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
$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,20 +46,10 @@ class AssetModelsController extends Controller
|
|||||||
'manufacturer',
|
'manufacturer',
|
||||||
'requestable',
|
'requestable',
|
||||||
'assets_count',
|
'assets_count',
|
||||||
'assets_assigned_count',
|
|
||||||
'assets_archived_count',
|
|
||||||
'remaining',
|
|
||||||
'category',
|
'category',
|
||||||
'fieldset',
|
'fieldset',
|
||||||
'deleted_at',
|
'deleted_at',
|
||||||
'updated_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([
|
$assetmodels = AssetModel::select([
|
||||||
@@ -79,31 +69,9 @@ class AssetModelsController extends Controller
|
|||||||
'models.fieldset_id',
|
'models.fieldset_id',
|
||||||
'models.deleted_at',
|
'models.deleted_at',
|
||||||
'models.updated_at',
|
'models.updated_at',
|
||||||
'models.require_serial'
|
|
||||||
])
|
])
|
||||||
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
|
->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') {
|
if ($request->input('status')=='deleted') {
|
||||||
$assetmodels->onlyTrashed();
|
$assetmodels->onlyTrashed();
|
||||||
@@ -117,12 +85,6 @@ class AssetModelsController extends Controller
|
|||||||
$assetmodels = $assetmodels->where('models.model_number', '=', $request->input('model_number'));
|
$assetmodels = $assetmodels->where('models.model_number', '=', $request->input('model_number'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->input('requestable') == 'true') {
|
|
||||||
$assetmodels = $assetmodels->where('models.requestable', '=', '1');
|
|
||||||
} elseif ($request->input('requestable') == 'false') {
|
|
||||||
$assetmodels = $assetmodels->where('models.requestable', '=', '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->filled('notes')) {
|
if ($request->filled('notes')) {
|
||||||
$assetmodels = $assetmodels->where('models.notes', '=', $request->input('notes'));
|
$assetmodels = $assetmodels->where('models.notes', '=', $request->input('notes'));
|
||||||
}
|
}
|
||||||
@@ -186,7 +148,7 @@ class AssetModelsController extends Controller
|
|||||||
$assetmodel = $request->handleImages($assetmodel);
|
$assetmodel = $request->handleImages($assetmodel);
|
||||||
|
|
||||||
if ($assetmodel->save()) {
|
if ($assetmodel->save()) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.create.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.create.success')));
|
||||||
}
|
}
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
|
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
|
||||||
|
|
||||||
@@ -239,7 +201,7 @@ class AssetModelsController extends Controller
|
|||||||
$assetmodel = AssetModel::findOrFail($id);
|
$assetmodel = AssetModel::findOrFail($id);
|
||||||
$assetmodel->fill($request->all());
|
$assetmodel->fill($request->all());
|
||||||
$assetmodel = $request->handleImages($assetmodel);
|
$assetmodel = $request->handleImages($assetmodel);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow custom_fieldset_id to override and populate fieldset_id.
|
* Allow custom_fieldset_id to override and populate fieldset_id.
|
||||||
* This is stupid, but required for legacy API support.
|
* This is stupid, but required for legacy API support.
|
||||||
@@ -254,7 +216,7 @@ class AssetModelsController extends Controller
|
|||||||
|
|
||||||
|
|
||||||
if ($assetmodel->save()) {
|
if ($assetmodel->save()) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.update.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.update.success')));
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
|
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use App\Events\CheckoutableCheckedIn;
|
|||||||
use App\Http\Requests\StoreAssetRequest;
|
use App\Http\Requests\StoreAssetRequest;
|
||||||
use App\Http\Requests\UpdateAssetRequest;
|
use App\Http\Requests\UpdateAssetRequest;
|
||||||
use App\Http\Traits\MigratesLegacyAssetLocations;
|
use App\Http\Traits\MigratesLegacyAssetLocations;
|
||||||
use App\Http\Transformers\ComponentsTransformer;
|
|
||||||
use App\Models\AccessoryCheckout;
|
use App\Models\AccessoryCheckout;
|
||||||
use App\Models\CheckoutAcceptance;
|
use App\Models\CheckoutAcceptance;
|
||||||
use App\Models\LicenseSeat;
|
use App\Models\LicenseSeat;
|
||||||
@@ -35,7 +34,6 @@ use Illuminate\Support\Facades\Log;
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\View\Label;
|
use App\View\Label;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,40 +113,17 @@ class AssetsController extends Controller
|
|||||||
'byod',
|
'byod',
|
||||||
'asset_eol_date',
|
'asset_eol_date',
|
||||||
'requestable',
|
'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 = [];
|
$filter = [];
|
||||||
|
|
||||||
if ($request->filled('filter')) {
|
if ($request->filled('filter')) {
|
||||||
$filter = json_decode($request->input('filter'), true);
|
$filter = json_decode($request->input('filter'), true);
|
||||||
|
}
|
||||||
|
|
||||||
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
|
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
|
||||||
return in_array($key, $allowed_columns);
|
foreach ($all_custom_fields as $field) {
|
||||||
}, ARRAY_FILTER_USE_KEY);
|
$allowed_columns[] = $field->db_column_name();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$assets = Asset::select('assets.*')
|
$assets = Asset::select('assets.*')
|
||||||
@@ -164,7 +139,6 @@ class AssetsController extends Controller
|
|||||||
'model.category',
|
'model.category',
|
||||||
'model.manufacturer',
|
'model.manufacturer',
|
||||||
'model.fieldset',
|
'model.fieldset',
|
||||||
'model.depreciation',
|
|
||||||
'supplier'
|
'supplier'
|
||||||
); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
|
); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
|
||||||
|
|
||||||
@@ -183,7 +157,7 @@ class AssetsController extends Controller
|
|||||||
// Search custom fields by column name
|
// Search custom fields by column name
|
||||||
foreach ($all_custom_fields as $field) {
|
foreach ($all_custom_fields as $field) {
|
||||||
if ($request->filled($field->db_column_name()) && $field->db_column_name()) {
|
if ($request->filled($field->db_column_name()) && $field->db_column_name()) {
|
||||||
$assets->where('assets.'.$field->db_column_name(), '=', $request->input($field->db_column_name()));
|
$assets->where($field->db_column_name(), '=', $request->input($field->db_column_name()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,15 +297,9 @@ class AssetsController extends Controller
|
|||||||
if ($request->input('requestable') == 'true') {
|
if ($request->input('requestable') == 'true') {
|
||||||
$assets->where('assets.requestable', '=', '1');
|
$assets->where('assets.requestable', '=', '1');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('model_id')) {
|
if ($request->filled('model_id')) {
|
||||||
// If model_id is already an array, just use it as-is
|
$assets->InModelList([$request->input('model_id')]);
|
||||||
if (is_array($request->input('model_id'))) {
|
|
||||||
$assets->InModelList($request->input('model_id'));
|
|
||||||
} else {
|
|
||||||
// Otherwise, turn it into an array
|
|
||||||
$assets->InModelList([$request->input('model_id')]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('category_id')) {
|
if ($request->filled('category_id')) {
|
||||||
@@ -420,9 +388,6 @@ class AssetsController extends Controller
|
|||||||
case 'assigned_to':
|
case 'assigned_to':
|
||||||
$assets->OrderAssigned($order);
|
$assets->OrderAssigned($order);
|
||||||
break;
|
break;
|
||||||
case 'jobtitle':
|
|
||||||
$assets->OrderByJobTitle($order);
|
|
||||||
break;
|
|
||||||
case 'created_by':
|
case 'created_by':
|
||||||
$assets->OrderByCreatedByName($order);
|
$assets->OrderByCreatedByName($order);
|
||||||
break;
|
break;
|
||||||
@@ -470,6 +435,12 @@ class AssetsController extends Controller
|
|||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here we're just determining which Transformer (via $transformer) to use based on the
|
||||||
|
* variables we set earlier on in this method - we default to AssetsTransformer.
|
||||||
|
*/
|
||||||
return (new $transformer)->transformAssets($assets, $total, $request);
|
return (new $transformer)->transformAssets($assets, $total, $request);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,32 +491,15 @@ class AssetsController extends Controller
|
|||||||
public function showBySerial(Request $request, $serial): JsonResponse | array
|
public function showBySerial(Request $request, $serial): JsonResponse | array
|
||||||
{
|
{
|
||||||
$this->authorize('index', Asset::class);
|
$this->authorize('index', Asset::class);
|
||||||
$assets = Asset::where('serial', $serial)->with([
|
$assets = Asset::where('serial', $serial)->with('assetstatus')->with('assignedTo');
|
||||||
'assetstatus',
|
|
||||||
'assignedTo',
|
|
||||||
'company',
|
|
||||||
'defaultLoc',
|
|
||||||
'location',
|
|
||||||
'model.category',
|
|
||||||
'model.depreciation',
|
|
||||||
'model.fieldset',
|
|
||||||
'model.manufacturer',
|
|
||||||
'supplier',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Check if they've passed ?deleted=true
|
// Check if they've passed ?deleted=true
|
||||||
if ($request->input('deleted', 'false') == 'true') {
|
if ($request->input('deleted', 'false') == 'true') {
|
||||||
$assets = $assets->withTrashed();
|
$assets = $assets->withTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
$offset = ($request->input('offset') > $assets->count()) ? $assets->count() : app('api_offset_value');
|
if (($assets = $assets->get()) && ($assets->count()) > 0) {
|
||||||
$limit = app('api_limit_value');
|
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
|
||||||
|
|
||||||
$total = $assets->count();
|
|
||||||
$assets = $assets->skip($offset)->take($limit)->get();
|
|
||||||
|
|
||||||
if (($assets) && ($assets->count()) > 0) {
|
|
||||||
return (new AssetsTransformer)->transformAssets($assets, $total);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are 0 results, return the "no such asset" response
|
// If there are 0 results, return the "no such asset" response
|
||||||
@@ -602,12 +556,7 @@ class AssetsController extends Controller
|
|||||||
'assets.assigned_to',
|
'assets.assigned_to',
|
||||||
'assets.assigned_type',
|
'assets.assigned_type',
|
||||||
'assets.status_id',
|
'assets.status_id',
|
||||||
])->with('model', 'assetstatus', 'assignedTo')
|
])->with('model', 'assetstatus', 'assignedTo')->NotArchived();
|
||||||
->NotArchived();
|
|
||||||
|
|
||||||
if ((Setting::getSettings()->full_multiple_companies_support=='1') && ($request->filled('companyId'))) {
|
|
||||||
$assets->where('assets.company_id', $request->input('companyId'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->filled('assetStatusType') && $request->input('assetStatusType') === 'RTD') {
|
if ($request->filled('assetStatusType') && $request->input('assetStatusType') === 'RTD') {
|
||||||
$assets = $assets->RTD();
|
$assets = $assets->RTD();
|
||||||
@@ -617,6 +566,7 @@ class AssetsController extends Controller
|
|||||||
$assets = $assets->AssignedSearch($request->input('search'));
|
$assets = $assets->AssignedSearch($request->input('search'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$assets = $assets->paginate(50);
|
$assets = $assets->paginate(50);
|
||||||
|
|
||||||
// Loop through and set some custom properties for the transformer to use.
|
// Loop through and set some custom properties for the transformer to use.
|
||||||
@@ -628,7 +578,7 @@ class AssetsController extends Controller
|
|||||||
$asset->use_text = $asset->present()->fullName;
|
$asset->use_text = $asset->present()->fullName;
|
||||||
|
|
||||||
if (($asset->checkedOutToUser()) && ($asset->assigned)) {
|
if (($asset->checkedOutToUser()) && ($asset->assigned)) {
|
||||||
$asset->use_text .= ' → ' . $asset->assigned->display_name;
|
$asset->use_text .= ' → ' . $asset->assigned->getFullNameAttribute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -729,9 +679,7 @@ class AssetsController extends Controller
|
|||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.create.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.create.success')));
|
||||||
|
|
||||||
// below is what we want the _eventual_ return to look like - in a more standardized format.
|
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
|
||||||
// return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
|
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
|
||||||
@@ -1099,7 +1047,7 @@ class AssetsController extends Controller
|
|||||||
* @param int $id
|
* @param int $id
|
||||||
* @since [v4.0]
|
* @since [v4.0]
|
||||||
*/
|
*/
|
||||||
public function audit(Request $request, Asset $asset): JsonResponse
|
public function audit(Request $request): JsonResponse
|
||||||
|
|
||||||
{
|
{
|
||||||
$this->authorize('audit', Asset::class);
|
$this->authorize('audit', Asset::class);
|
||||||
@@ -1107,15 +1055,36 @@ class AssetsController extends Controller
|
|||||||
$settings = Setting::getSettings();
|
$settings = Setting::getSettings();
|
||||||
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
|
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
|
||||||
|
|
||||||
// Allow the asset tag to be passed in the payload (legacy method)
|
// No tag passed - return an error
|
||||||
if ($request->filled('asset_tag')) {
|
if (!$request->filled('asset_tag')) {
|
||||||
$asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first();
|
return response()->json(Helper::formatStandardApiResponse('error', [
|
||||||
|
'asset_tag' => '',
|
||||||
|
'error' => trans('admin/hardware/message.no_tag'),
|
||||||
|
], trans('admin/hardware/message.no_tag')), 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first();
|
||||||
|
|
||||||
|
|
||||||
if ($asset) {
|
if ($asset) {
|
||||||
|
|
||||||
$originalValues = $asset->getRawOriginal();
|
/**
|
||||||
|
* Even though we do a save() further down, we don't want to log this as a "normal" asset update,
|
||||||
|
* which would trigger the Asset Observer and would log an asset *update* log entry (because the
|
||||||
|
* de-normed fields like next_audit_date on the asset itself will change on save()) *in addition* to
|
||||||
|
* the audit log entry we're creating through this controller.
|
||||||
|
*
|
||||||
|
* To prevent this double-logging (one for update and one for audit), we skip the observer and bypass
|
||||||
|
* that de-normed update log entry by using unsetEventDispatcher(), BUT invoking unsetEventDispatcher()
|
||||||
|
* will bypass normal model-level validation that's usually handled at the observer )
|
||||||
|
*
|
||||||
|
* We handle validation on the save() by checking if the asset is valid via the ->isValid() method,
|
||||||
|
* which manually invokes Watson Validating to make sure the asset's model is valid.
|
||||||
|
*
|
||||||
|
* @see \App\Observers\AssetObserver::updating()
|
||||||
|
*/
|
||||||
|
$asset->unsetEventDispatcher();
|
||||||
$asset->next_audit_date = $dt;
|
$asset->next_audit_date = $dt;
|
||||||
|
|
||||||
if ($request->filled('next_audit_date')) {
|
if ($request->filled('next_audit_date')) {
|
||||||
@@ -1130,89 +1099,33 @@ class AssetsController extends Controller
|
|||||||
|
|
||||||
$asset->last_audit_date = date('Y-m-d H:i:s');
|
$asset->last_audit_date = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
// Set up the payload for re-display in the API response
|
|
||||||
$payload = [
|
|
||||||
'id' => $asset->id,
|
|
||||||
'asset_tag' => $asset->asset_tag,
|
|
||||||
'note' => $request->input('note'),
|
|
||||||
'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date),
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 (($asset->model) && ($asset->model->fieldset)) {
|
|
||||||
$payload['custom_fields'] = [];
|
|
||||||
foreach ($asset->model->fieldset->fields as $field) {
|
|
||||||
if (($field->display_audit=='1') && ($request->has($field->db_column))) {
|
|
||||||
if ($field->field_encrypted == '1') {
|
|
||||||
if (Gate::allows('assets.view.encrypted_custom_fields')) {
|
|
||||||
if (is_array($request->input($field->db_column))) {
|
|
||||||
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
|
|
||||||
} else {
|
|
||||||
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (is_array($request->input($field->db_column))) {
|
|
||||||
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
|
|
||||||
} else {
|
|
||||||
$asset->{$field->db_column} = $request->input($field->db_column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$payload['custom_fields'][$field->db_column] = $request->input($field->db_column);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invoke the validation to see if the audit will complete successfully
|
|
||||||
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
|
|
||||||
|
|
||||||
// Validate the rest of the data before we turn off the event dispatcher
|
|
||||||
if ($asset->isInvalid()) {
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', ['asset_tag' => $asset->asset_tag], $asset->getErrors()));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Even though we do a save() further down, we don't want to log this as a "normal" asset update,
|
|
||||||
* which would trigger the Asset Observer and would log an asset *update* log entry (because the
|
|
||||||
* de-normed fields like next_audit_date on the asset itself will change on save()) *in addition* to
|
|
||||||
* the audit log entry we're creating through this controller.
|
|
||||||
*
|
|
||||||
* To prevent this double-logging (one for update and one for audit), we skip the observer and bypass
|
|
||||||
* that de-normed update log entry by using unsetEventDispatcher(), BUT invoking unsetEventDispatcher()
|
|
||||||
* will bypass normal model-level validation that's usually handled at the observer)
|
|
||||||
*
|
|
||||||
* We handle validation on the save() by checking if the asset is valid via the ->isValid() method,
|
|
||||||
* which manually invokes Watson Validating to make sure the asset's model is valid.
|
|
||||||
*
|
|
||||||
* @see \App\Observers\AssetObserver::updating()
|
|
||||||
* @see \App\Models\Asset::save()
|
|
||||||
*/
|
|
||||||
|
|
||||||
$asset->unsetEventDispatcher();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke Watson Validating to check the asset itself and check to make sure it saved correctly.
|
* Invoke Watson Validating to check the asset itself and check to make sure it saved correctly.
|
||||||
* We have to invoke this manually because of the unsetEventDispatcher() above.)
|
* We have to invoke this manually because of the unsetEventDispatcher() above.)
|
||||||
*/
|
*/
|
||||||
if ($asset->isValid() && $asset->save()) {
|
if ($asset->isValid() && $asset->save()) {
|
||||||
$asset->logAudit(request('note'), request('location_id'), null, $originalValues);
|
$asset->logAudit(request('note'), request('location_id'));
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/hardware/message.audit.success')));
|
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('success', [
|
||||||
|
'asset_tag' => e($asset->asset_tag),
|
||||||
|
'note' => e($request->input('note')),
|
||||||
|
'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date),
|
||||||
|
], trans('admin/hardware/message.audit.success')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Asset failed validation or was not able to be saved
|
||||||
|
return response()->json(Helper::formatStandardApiResponse('error', [
|
||||||
|
'asset_tag' => e($asset->asset_tag),
|
||||||
|
'error' => $asset->getErrors()->first(),
|
||||||
|
], trans('admin/hardware/message.audit.error', ['error' => $asset->getErrors()->first()])), 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// No matching asset for the asset tag that was passed.
|
// No matching asset for the asset tag that was passed.
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
|
return response()->json(Helper::formatStandardApiResponse('error', [
|
||||||
|
'asset_tag' => e($request->input('asset_tag')),
|
||||||
|
'error' => trans('admin/hardware/message.audit.error'),
|
||||||
|
], trans('admin/hardware/message.audit.error', ['error' => trans('admin/hardware/message.does_not_exist')])), 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1308,19 +1221,9 @@ class AssetsController extends Controller
|
|||||||
|
|
||||||
public function assignedAssets(Request $request, Asset $asset) : JsonResponse | array
|
public function assignedAssets(Request $request, Asset $asset) : JsonResponse | array
|
||||||
{
|
{
|
||||||
$this->authorize('view', Asset::class);
|
|
||||||
$this->authorize('view', $asset);
|
|
||||||
|
|
||||||
$query = Asset::where([
|
return [];
|
||||||
'assigned_to' => $asset->id,
|
// to do
|
||||||
'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
|
public function assignedAccessories(Request $request, Asset $asset) : JsonResponse | array
|
||||||
@@ -1340,18 +1243,6 @@ class AssetsController extends Controller
|
|||||||
return (new AssetsTransformer)->transformCheckedoutAccessories($accessory_checkouts, $total);
|
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
|
* Generate asset labels by tag
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Actions\Categories\DestroyCategoryAction;
|
|
||||||
use App\Exceptions\ItemStillHasChildren;
|
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Transformers\CategoriesTransformer;
|
use App\Http\Transformers\CategoriesTransformer;
|
||||||
@@ -40,8 +38,6 @@ class CategoriesController extends Controller
|
|||||||
'consumables_count',
|
'consumables_count',
|
||||||
'components_count',
|
'components_count',
|
||||||
'licenses_count',
|
'licenses_count',
|
||||||
'created_at',
|
|
||||||
'updated_at',
|
|
||||||
'image',
|
'image',
|
||||||
'notes',
|
'notes',
|
||||||
];
|
];
|
||||||
@@ -60,26 +56,9 @@ class CategoriesController extends Controller
|
|||||||
'notes',
|
'notes',
|
||||||
])
|
])
|
||||||
->with('adminuser')
|
->with('adminuser')
|
||||||
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count');
|
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_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.
|
* 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
|
* We don't currently use it within the Snipe-IT GUI, but will be useful for API integrations where they
|
||||||
@@ -93,6 +72,10 @@ class CategoriesController extends Controller
|
|||||||
$categories = $categories->withCount('showableAssets as assets_count');
|
$categories = $categories->withCount('showableAssets as assets_count');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$categories = $categories->TextSearch($request->input('search'));
|
||||||
|
}
|
||||||
|
|
||||||
if ($request->filled('name')) {
|
if ($request->filled('name')) {
|
||||||
$categories->where('name', '=', $request->input('name'));
|
$categories->where('name', '=', $request->input('name'));
|
||||||
}
|
}
|
||||||
@@ -226,21 +209,17 @@ class CategoriesController extends Controller
|
|||||||
* @param int $id
|
* @param int $id
|
||||||
* @return \Illuminate\Http\Response
|
* @return \Illuminate\Http\Response
|
||||||
*/
|
*/
|
||||||
public function destroy(Category $category): JsonResponse
|
public function destroy($id) : JsonResponse
|
||||||
{
|
{
|
||||||
$this->authorize('delete', Category::class);
|
$this->authorize('delete', Category::class);
|
||||||
try {
|
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count')->findOrFail($id);
|
||||||
DestroyCategoryAction::run(category: $category);
|
|
||||||
} catch (ItemStillHasChildren $e) {
|
if (! $category->isDeletable()) {
|
||||||
return response()->json(
|
return response()->json(
|
||||||
Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.general_assoc_warning', ['asset_type' => $category->category_type]))
|
Helper::formatStandardApiResponse('error', null, trans('admin/categories/message.assoc_items', ['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')));
|
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/categories/message.delete.success')));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
|
||||||
|
|
||||||
use App\Actions\CheckoutRequests\CancelCheckoutRequestAction;
|
|
||||||
use App\Actions\CheckoutRequests\CreateCheckoutRequestAction;
|
|
||||||
use App\Exceptions\AssetNotRequestable;
|
|
||||||
use App\Helpers\Helper;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Models\Asset;
|
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class CheckoutRequest extends Controller
|
|
||||||
{
|
|
||||||
public function store(Asset $asset): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
CreateCheckoutRequestAction::run($asset, auth()->user());
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.requests.success')));
|
|
||||||
} catch (AssetNotRequestable $e) {
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', 'Asset is not requestable'));
|
|
||||||
} catch (AuthorizationException $e) {
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.insufficient_permissions')));
|
|
||||||
} catch (Exception $e) {
|
|
||||||
report($e);
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(Asset $asset): JsonResponse
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
CancelCheckoutRequestAction::run($asset, auth()->user());
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.requests.canceled')));
|
|
||||||
} catch (AuthorizationException $e) {
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.insufficient_permissions')));
|
|
||||||
} catch (Exception $e) {
|
|
||||||
report($e);
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -43,10 +43,7 @@ class CompaniesController extends Controller
|
|||||||
|
|
||||||
$companies = Company::withCount(['assets as assets_count' => function ($query) {
|
$companies = Company::withCount(['assets as assets_count' => function ($query) {
|
||||||
$query->AssetsForShow();
|
$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')) {
|
if ($request->filled('search')) {
|
||||||
$companies->TextSearch($request->input('search'));
|
$companies->TextSearch($request->input('search'));
|
||||||
@@ -122,7 +119,6 @@ class CompaniesController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('view', Company::class);
|
$this->authorize('view', Company::class);
|
||||||
$company = Company::findOrFail($id);
|
$company = Company::findOrFail($id);
|
||||||
$this->authorize('view', $company);
|
|
||||||
return (new CompaniesTransformer)->transformCompany($company);
|
return (new CompaniesTransformer)->transformCompany($company);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -140,7 +136,6 @@ class CompaniesController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('update', Company::class);
|
$this->authorize('update', Company::class);
|
||||||
$company = Company::findOrFail($id);
|
$company = Company::findOrFail($id);
|
||||||
$this->authorize('update', $company);
|
|
||||||
$company->fill($request->all());
|
$company->fill($request->all());
|
||||||
$company = $request->handleImages($company);
|
$company = $request->handleImages($company);
|
||||||
|
|
||||||
@@ -193,7 +188,6 @@ class CompaniesController extends Controller
|
|||||||
'companies.image',
|
'companies.image',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->get('search').'%');
|
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->get('search').'%');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,46 +45,22 @@ class ComponentsController extends Controller
|
|||||||
'qty',
|
'qty',
|
||||||
'image',
|
'image',
|
||||||
'notes',
|
'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.*')
|
$components = Component::select('components.*')
|
||||||
->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer', 'uncontrainedAssets')
|
->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer', 'uncontrainedAssets')
|
||||||
->withSum('uncontrainedAssets', 'components_assets.assigned_qty');
|
->withSum('uncontrainedAssets', 'components_assets.assigned_qty');
|
||||||
|
|
||||||
$filter = [];
|
if ($request->filled('search')) {
|
||||||
|
$components = $components->TextSearch($request->input('search'));
|
||||||
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')) {
|
if ($request->filled('name')) {
|
||||||
$components->where('name', '=', $request->input('name'));
|
$components->where('name', '=', $request->input('name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('company_id')) {
|
if ($request->filled('company_id')) {
|
||||||
$components->where('components.company_id', '=', $request->input('company_id'));
|
$components->where('company_id', '=', $request->input('company_id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('category_id')) {
|
if ($request->filled('category_id')) {
|
||||||
|
|||||||
@@ -31,59 +31,16 @@ class ConsumablesController extends Controller
|
|||||||
$consumables = Consumable::with('company', 'location', 'category', 'supplier', 'manufacturer')
|
$consumables = Consumable::with('company', 'location', 'category', 'supplier', 'manufacturer')
|
||||||
->withCount('users as consumables_users_count');
|
->withCount('users as consumables_users_count');
|
||||||
|
|
||||||
// This array is what determines which fields should be allowed to be sorted on ON the table itself.
|
if ($request->filled('search')) {
|
||||||
// These must match a column on the consumables table directly.
|
$consumables = $consumables->TextSearch(e($request->input('search')));
|
||||||
$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')) {
|
if ($request->filled('name')) {
|
||||||
$consumables->where('name', '=', $request->input('name'));
|
$consumables->where('name', '=', $request->input('name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('company_id')) {
|
if ($request->filled('company_id')) {
|
||||||
$consumables->where('consumables.company_id', '=', $request->input('company_id'));
|
$consumables->where('company_id', '=', $request->input('company_id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('category_id')) {
|
if ($request->filled('category_id')) {
|
||||||
@@ -139,6 +96,25 @@ class ConsumablesController extends Controller
|
|||||||
$consumables = $consumables->OrderByCreatedBy($order);
|
$consumables = $consumables->OrderByCreatedBy($order);
|
||||||
break;
|
break;
|
||||||
default:
|
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';
|
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||||
$consumables = $consumables->orderBy($sort, $order);
|
$consumables = $consumables->orderBy($sort, $order);
|
||||||
break;
|
break;
|
||||||
@@ -252,16 +228,11 @@ class ConsumablesController extends Controller
|
|||||||
foreach ($consumable->consumableAssignments as $consumable_assignment) {
|
foreach ($consumable->consumableAssignments as $consumable_assignment) {
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '',
|
'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '',
|
||||||
'user' => ($consumable_assignment->user) ? [
|
'name' => ($consumable_assignment->user) ? $consumable_assignment->user->present()->nameUrl() : 'Deleted User',
|
||||||
'id' => (int) $consumable_assignment->user->id,
|
|
||||||
'name'=> e($consumable_assignment->user->display_name),
|
|
||||||
] : null,
|
|
||||||
'created_at' => Helper::getFormattedDateObject($consumable_assignment->created_at, 'datetime'),
|
'created_at' => Helper::getFormattedDateObject($consumable_assignment->created_at, 'datetime'),
|
||||||
'note' => ($consumable_assignment->note) ? e($consumable_assignment->note) : null,
|
'note' => ($consumable_assignment->note) ? e($consumable_assignment->note) : null,
|
||||||
'created_by' => ($consumable_assignment->adminuser) ? [
|
'admin' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null, // legacy, so we don't change the shape of the response
|
||||||
'id' => (int) $consumable_assignment->adminuser->id,
|
'created_by' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null,
|
||||||
'name'=> e($consumable_assignment->adminuser->display_name),
|
|
||||||
] : null,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class GroupsController extends Controller
|
|||||||
$this->authorize('superadmin');
|
$this->authorize('superadmin');
|
||||||
$group = new Group;
|
$group = new Group;
|
||||||
// Get all the available permissions
|
// Get all the available permissions
|
||||||
$permissions = json_encode(config('permissions'));
|
$permissions = config('permissions');
|
||||||
$groupPermissions = Helper::selectedPermissionsArray($permissions, $permissions);
|
$groupPermissions = Helper::selectedPermissionsArray($permissions, $permissions);
|
||||||
|
|
||||||
$group->name = $request->input('name');
|
$group->name = $request->input('name');
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class ImportController extends Controller
|
|||||||
if (function_exists('iconv')) {
|
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?
|
$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);
|
$encoding = $detector->getEncoding($file_contents);
|
||||||
\Log::debug("Discovered encoding: $encoding in uploaded CSV");
|
\Log::warning("Discovered encoding: $encoding in uploaded CSV");
|
||||||
$reader = null;
|
$reader = null;
|
||||||
if (strcasecmp($encoding, 'UTF-8') != 0) {
|
if (strcasecmp($encoding, 'UTF-8') != 0) {
|
||||||
$transliterated = false;
|
$transliterated = false;
|
||||||
@@ -103,7 +103,7 @@ class ImportController extends Controller
|
|||||||
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
|
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$import->header_row = $reader->nth(0);
|
$import->header_row = $reader->fetchOne(0);
|
||||||
} catch (JsonEncodingException $e) {
|
} catch (JsonEncodingException $e) {
|
||||||
return response()->json(
|
return response()->json(
|
||||||
Helper::formatStandardApiResponse(
|
Helper::formatStandardApiResponse(
|
||||||
@@ -136,7 +136,7 @@ class ImportController extends Controller
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Grab the first row to display via ajax as the user picks fields
|
// Grab the first row to display via ajax as the user picks fields
|
||||||
$import->first_row = $reader->nth(1);
|
$import->first_row = $reader->fetchOne(1);
|
||||||
} catch (JsonEncodingException $e) {
|
} catch (JsonEncodingException $e) {
|
||||||
return response()->json(
|
return response()->json(
|
||||||
Helper::formatStandardApiResponse(
|
Helper::formatStandardApiResponse(
|
||||||
@@ -195,7 +195,7 @@ class ImportController extends Controller
|
|||||||
// Run a backup immediately before processing
|
// Run a backup immediately before processing
|
||||||
if ($request->get('run-backup')) {
|
if ($request->get('run-backup')) {
|
||||||
Log::debug('Backup manually requested via importer');
|
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 {
|
} else {
|
||||||
Log::debug('NO BACKUP requested via importer');
|
Log::debug('NO BACKUP requested via importer');
|
||||||
}
|
}
|
||||||
@@ -234,15 +234,6 @@ class ImportController extends Controller
|
|||||||
case 'location':
|
case 'location':
|
||||||
$redirectTo = 'locations.index';
|
$redirectTo = 'locations.index';
|
||||||
break;
|
break;
|
||||||
case 'supplier':
|
|
||||||
$redirectTo = 'suppliers.index';
|
|
||||||
break;
|
|
||||||
case 'manufacturer':
|
|
||||||
$redirectTo = 'manufacturers.index';
|
|
||||||
break;
|
|
||||||
case 'category':
|
|
||||||
$redirectTo = 'categories.index';
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($errors) { //Failure
|
if ($errors) { //Failure
|
||||||
|
|||||||
@@ -29,21 +29,12 @@ class LicenseSeatsController extends Controller
|
|||||||
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
|
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
|
||||||
->where('license_seats.license_id', $licenseId);
|
->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';
|
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||||
|
|
||||||
if ($request->input('sort') == 'department') {
|
if ($request->input('sort') == 'department') {
|
||||||
$seats->OrderDepartments($order);
|
$seats->OrderDepartments($order);
|
||||||
} else {
|
} else {
|
||||||
$seats->orderBy('updated_at', $order);
|
$seats->orderBy('id', $order);
|
||||||
}
|
}
|
||||||
|
|
||||||
$total = $seats->count();
|
$total = $seats->count();
|
||||||
@@ -128,9 +119,7 @@ class LicenseSeatsController extends Controller
|
|||||||
// nothing to update
|
// nothing to update
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
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,
|
// 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...
|
// we simply let assets take precedence over users...
|
||||||
if ($licenseSeat->isDirty('assigned_to')) {
|
if ($licenseSeat->isDirty('assigned_to')) {
|
||||||
@@ -147,17 +136,13 @@ class LicenseSeatsController extends Controller
|
|||||||
if ($licenseSeat->save()) {
|
if ($licenseSeat->save()) {
|
||||||
|
|
||||||
if ($is_checkin) {
|
if ($is_checkin) {
|
||||||
if(!$licenseSeat->license->reassignable){
|
$licenseSeat->logCheckin($target, $request->input('note'));
|
||||||
$licenseSeat->unreassignable_seat = true;
|
|
||||||
$licenseSeat->save();
|
|
||||||
}
|
|
||||||
$licenseSeat->logCheckin($target, $licenseSeat->notes);
|
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// in this case, relevant fields are touched but it's not a checkin operation. so it must be a checkout operation.
|
// in this case, relevant fields are touched but it's not a checkin operation. so it must be a checkout operation.
|
||||||
$licenseSeat->logCheckout($request->input('notes'), $target);
|
$licenseSeat->logCheckout($request->input('note'), $target);
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Http\Transformers\LicensesTransformer;
|
use App\Http\Transformers\LicensesTransformer;
|
||||||
use App\Http\Transformers\SelectlistTransformer;
|
use App\Http\Transformers\SelectlistTransformer;
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -26,15 +25,6 @@ class LicensesController extends Controller
|
|||||||
$this->authorize('view', License::class);
|
$this->authorize('view', License::class);
|
||||||
|
|
||||||
$licenses = License::with('company', 'manufacturer', 'supplier','category', 'adminuser')->withCount('freeSeats as free_seats_count');
|
$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')) {
|
if ($request->filled('company_id')) {
|
||||||
$licenses->where('licenses.company_id', '=', $request->input('company_id'));
|
$licenses->where('licenses.company_id', '=', $request->input('company_id'));
|
||||||
@@ -104,8 +94,6 @@ class LicensesController extends Controller
|
|||||||
$licenses->onlyTrashed();
|
$licenses->onlyTrashed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
// 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');
|
$offset = ($request->input('offset') > $licenses->count()) ? $licenses->count() : app('api_offset_value');
|
||||||
$limit = app('api_limit_value');
|
$limit = app('api_limit_value');
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ use App\Http\Transformers\SelectlistTransformer;
|
|||||||
use App\Models\Accessory;
|
use App\Models\Accessory;
|
||||||
use App\Models\AccessoryCheckout;
|
use App\Models\AccessoryCheckout;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\Company;
|
|
||||||
use App\Models\Location;
|
use App\Models\Location;
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
@@ -37,14 +35,10 @@ class LocationsController extends Controller
|
|||||||
'address',
|
'address',
|
||||||
'address2',
|
'address2',
|
||||||
'assets_count',
|
'assets_count',
|
||||||
'assigned_assets_count',
|
'assets_count',
|
||||||
'rtd_assets_count',
|
|
||||||
'accessories_count',
|
|
||||||
'assigned_accessories_count',
|
'assigned_accessories_count',
|
||||||
'components_count',
|
'assigned_assets_count',
|
||||||
'consumables_count',
|
'assigned_assets_count',
|
||||||
'users_count',
|
|
||||||
'children_count',
|
|
||||||
'city',
|
'city',
|
||||||
'country',
|
'country',
|
||||||
'created_at',
|
'created_at',
|
||||||
@@ -52,12 +46,12 @@ class LocationsController extends Controller
|
|||||||
'id',
|
'id',
|
||||||
'image',
|
'image',
|
||||||
'ldap_ou',
|
'ldap_ou',
|
||||||
'company_id',
|
|
||||||
'manager_id',
|
'manager_id',
|
||||||
'name',
|
'name',
|
||||||
'rtd_assets_count',
|
'rtd_assets_count',
|
||||||
'state',
|
'state',
|
||||||
'updated_at',
|
'updated_at',
|
||||||
|
'users_count',
|
||||||
'zip',
|
'zip',
|
||||||
'notes',
|
'notes',
|
||||||
];
|
];
|
||||||
@@ -80,10 +74,7 @@ class LocationsController extends Controller
|
|||||||
'locations.image',
|
'locations.image',
|
||||||
'locations.ldap_ou',
|
'locations.ldap_ou',
|
||||||
'locations.currency',
|
'locations.currency',
|
||||||
'locations.company_id',
|
|
||||||
'locations.notes',
|
'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('assets as assets_count')
|
||||||
@@ -91,15 +82,7 @@ class LocationsController extends Controller
|
|||||||
->withCount('accessories as accessories_count')
|
->withCount('accessories as accessories_count')
|
||||||
->withCount('rtd_assets as rtd_assets_count')
|
->withCount('rtd_assets as rtd_assets_count')
|
||||||
->withCount('children as children_count')
|
->withCount('children as children_count')
|
||||||
->withCount('users as users_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
|
|
||||||
if (Setting::getSettings()->scope_locations_fmcs) {
|
|
||||||
$locations = Company::scopeCompanyables($locations);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$locations = $locations->TextSearch($request->input('search'));
|
$locations = $locations->TextSearch($request->input('search'));
|
||||||
@@ -133,18 +116,6 @@ class LocationsController extends Controller
|
|||||||
$locations->where('locations.manager_id', '=', $request->input('manager_id'));
|
$locations->where('locations.manager_id', '=', $request->input('manager_id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('company_id')) {
|
|
||||||
$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
|
// 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');
|
$offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value');
|
||||||
$limit = app('api_limit_value');
|
$limit = app('api_limit_value');
|
||||||
@@ -161,9 +132,6 @@ class LocationsController extends Controller
|
|||||||
case 'manager':
|
case 'manager':
|
||||||
$locations->OrderManager($order);
|
$locations->OrderManager($order);
|
||||||
break;
|
break;
|
||||||
case 'company':
|
|
||||||
$locations->OrderCompany($order);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
$locations->orderBy($sort, $order);
|
$locations->orderBy($sort, $order);
|
||||||
break;
|
break;
|
||||||
@@ -191,15 +159,6 @@ class LocationsController extends Controller
|
|||||||
$location->fill($request->all());
|
$location->fill($request->all());
|
||||||
$location = $request->handleImages($location);
|
$location = $request->handleImages($location);
|
||||||
|
|
||||||
// Only scope location if the setting is enabled
|
|
||||||
if (Setting::getSettings()->scope_locations_fmcs) {
|
|
||||||
$location->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
|
||||||
// check if parent is set and has a different company
|
|
||||||
if ($location->parent_id && Location::find($location->parent_id)->company_id != $location->company_id) {
|
|
||||||
response()->json(Helper::formatStandardApiResponse('error', null, 'different company than parent'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($location->save()) {
|
if ($location->save()) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', (new LocationsTransformer)->transformLocation($location), trans('admin/locations/message.create.success')));
|
return response()->json(Helper::formatStandardApiResponse('success', (new LocationsTransformer)->transformLocation($location), trans('admin/locations/message.create.success')));
|
||||||
}
|
}
|
||||||
@@ -217,7 +176,7 @@ class LocationsController extends Controller
|
|||||||
public function show($id) : JsonResponse | array
|
public function show($id) : JsonResponse | array
|
||||||
{
|
{
|
||||||
$this->authorize('view', Location::class);
|
$this->authorize('view', Location::class);
|
||||||
$location = Location::with('parent', 'manager', 'children', 'company')
|
$location = Location::with('parent', 'manager', 'children')
|
||||||
->select([
|
->select([
|
||||||
'locations.id',
|
'locations.id',
|
||||||
'locations.name',
|
'locations.name',
|
||||||
@@ -233,18 +192,12 @@ class LocationsController extends Controller
|
|||||||
'locations.updated_at',
|
'locations.updated_at',
|
||||||
'locations.image',
|
'locations.image',
|
||||||
'locations.currency',
|
'locations.currency',
|
||||||
'locations.company_id',
|
|
||||||
'locations.notes',
|
'locations.notes',
|
||||||
])
|
])
|
||||||
->withCount('assignedAssets as assigned_assets_count')
|
->withCount('assignedAssets as assigned_assets_count')
|
||||||
->withCount('assets as 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('rtd_assets as rtd_assets_count')
|
||||||
->withCount('children as children_count')
|
|
||||||
->withCount('users as users_count')
|
->withCount('users as users_count')
|
||||||
->withCount('consumables as consumables_count')
|
|
||||||
->withCount('components as components_count')
|
|
||||||
->findOrFail($id);
|
->findOrFail($id);
|
||||||
|
|
||||||
return (new LocationsTransformer)->transformLocation($location);
|
return (new LocationsTransformer)->transformLocation($location);
|
||||||
@@ -267,19 +220,6 @@ class LocationsController extends Controller
|
|||||||
$location->fill($request->all());
|
$location->fill($request->all());
|
||||||
$location = $request->handleImages($location);
|
$location = $request->handleImages($location);
|
||||||
|
|
||||||
if ($request->filled('company_id')) {
|
|
||||||
// Only scope location if the setting is enabled
|
|
||||||
if (Setting::getSettings()->scope_locations_fmcs) {
|
|
||||||
$location->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
|
||||||
// check if there are related objects with different company
|
|
||||||
if (Helper::test_locations_fmcs(false, $id, $location->company_id)) {
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'error scoped locations'));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$location->company_id = $request->get('company_id');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($location->isValid()) {
|
if ($location->isValid()) {
|
||||||
|
|
||||||
$location->save();
|
$location->save();
|
||||||
@@ -339,15 +279,11 @@ class LocationsController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('delete', Location::class);
|
$this->authorize('delete', Location::class);
|
||||||
$location = Location::withCount('assignedAssets as assigned_assets_count')
|
$location = Location::withCount('assignedAssets as assigned_assets_count')
|
||||||
->withCount('assignedAssets as assigned_assets_count')
|
|
||||||
->withCount('assets as 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('rtd_assets as rtd_assets_count')
|
||||||
->withCount('children as children_count')
|
->withCount('children as children_count')
|
||||||
->withCount('users as users_count')
|
->withCount('users as users_count')
|
||||||
->withCount('consumables as consumables_count')
|
->withCount('accessories as accessories_count')
|
||||||
->withCount('components as components_count')
|
|
||||||
->findOrFail($id);
|
->findOrFail($id);
|
||||||
|
|
||||||
if (! $location->isDeletable()) {
|
if (! $location->isDeletable()) {
|
||||||
@@ -404,11 +340,6 @@ class LocationsController extends Controller
|
|||||||
'locations.image',
|
'locations.image',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Only scope locations if the setting is enabled
|
|
||||||
if (Setting::getSettings()->scope_locations_fmcs) {
|
|
||||||
$locations = Company::scopeCompanyables($locations);
|
|
||||||
}
|
|
||||||
|
|
||||||
$page = 1;
|
$page = 1;
|
||||||
if ($request->filled('page')) {
|
if ($request->filled('page')) {
|
||||||
$page = $request->input('page');
|
$page = $request->input('page');
|
||||||
|
|||||||
@@ -2,13 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
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\Helpers\Helper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Transformers\ManufacturersTransformer;
|
use App\Http\Transformers\ManufacturersTransformer;
|
||||||
@@ -191,19 +184,19 @@ class ManufacturersController extends Controller
|
|||||||
* @since [v4.0]
|
* @since [v4.0]
|
||||||
* @param int $id
|
* @param int $id
|
||||||
*/
|
*/
|
||||||
public function destroy(Manufacturer $manufacturer): JsonResponse
|
public function destroy($id) : JsonResponse
|
||||||
{
|
{
|
||||||
|
$this->authorize('delete', Manufacturer::class);
|
||||||
|
$manufacturer = Manufacturer::findOrFail($id);
|
||||||
$this->authorize('delete', $manufacturer);
|
$this->authorize('delete', $manufacturer);
|
||||||
try {
|
|
||||||
DeleteManufacturerAction::run($manufacturer);
|
if ($manufacturer->isDeletable()) {
|
||||||
} catch (ItemStillHasChildren $e) {
|
$manufacturer->delete();
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.general_assoc_warning', ['item' => trans('general.manufacturer')])));
|
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/manufacturers/message.delete.success')));
|
||||||
} catch (\Exception $e) {
|
|
||||||
report($e);
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/manufacturers/message.delete.success')));
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/manufacturers/message.assoc_users')));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
<?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,19 +4,15 @@ namespace App\Http\Controllers\Api;
|
|||||||
|
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Transformers\ProfileTransformer;
|
|
||||||
use App\Models\CheckoutRequest;
|
use App\Models\CheckoutRequest;
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Laravel\Passport\TokenRepository;
|
use Laravel\Passport\TokenRepository;
|
||||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use App\Models\CustomField;
|
use App\Models\CustomField;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
||||||
|
|
||||||
class ProfileController extends Controller
|
class ProfileController extends Controller
|
||||||
{
|
{
|
||||||
@@ -69,7 +65,7 @@ class ProfileController extends Controller
|
|||||||
if ($checkoutRequest && $checkoutRequest->itemRequested()) {
|
if ($checkoutRequest && $checkoutRequest->itemRequested()) {
|
||||||
$assets = [
|
$assets = [
|
||||||
'image' => e($checkoutRequest->itemRequested()->present()->getImageUrl()),
|
'image' => e($checkoutRequest->itemRequested()->present()->getImageUrl()),
|
||||||
'name' => e($checkoutRequest->itemRequested()->display_name),
|
'name' => e($checkoutRequest->itemRequested()->present()->name()),
|
||||||
'type' => e($checkoutRequest->itemType()),
|
'type' => e($checkoutRequest->itemType()),
|
||||||
'qty' => (int) $checkoutRequest->quantity,
|
'qty' => (int) $checkoutRequest->quantity,
|
||||||
'location' => ($checkoutRequest->location()) ? e($checkoutRequest->location()->name) : null,
|
'location' => ($checkoutRequest->location()) ? e($checkoutRequest->location()->name) : null,
|
||||||
@@ -171,22 +167,6 @@ 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())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ namespace App\Http\Controllers\Api;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Transformers\ActionlogsTransformer;
|
use App\Http\Transformers\ActionlogsTransformer;
|
||||||
use App\Models\Actionlog;
|
use App\Models\Actionlog;
|
||||||
use App\Models\Company;
|
|
||||||
use App\Models\Setting;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
@@ -20,11 +18,10 @@ class ReportsController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index(Request $request) : JsonResponse | array
|
public function index(Request $request) : JsonResponse | array
|
||||||
{
|
{
|
||||||
$this->authorize('activity.view');
|
$this->authorize('reports.view');
|
||||||
|
|
||||||
$actionlogs = Actionlog::with('item', 'user', 'adminuser', 'target', 'location');
|
$actionlogs = Actionlog::with('item', 'user', 'adminuser', 'target', 'location');
|
||||||
|
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$actionlogs = $actionlogs->TextSearch(e($request->input('search')));
|
$actionlogs = $actionlogs->TextSearch(e($request->input('search')));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
|
use App\Helpers\StorageHelper;
|
||||||
use App\Http\Transformers\DatatablesTransformer;
|
use App\Http\Transformers\DatatablesTransformer;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
@@ -50,22 +51,10 @@ class SettingsController extends Controller
|
|||||||
})->slice(0, 10)->map(function ($item) use ($settings) {
|
})->slice(0, 10)->map(function ($item) use ($settings) {
|
||||||
return (object) [
|
return (object) [
|
||||||
'username' => $item[$settings['ldap_username_field']][0] ?? null,
|
'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,
|
'employee_number' => $item[$settings['ldap_emp_num']][0] ?? null,
|
||||||
'lastname' => $item[$settings['ldap_lname_field']][0] ?? null,
|
'lastname' => $item[$settings['ldap_lname_field']][0] ?? null,
|
||||||
'firstname' => $item[$settings['ldap_fname_field']][0] ?? null,
|
'firstname' => $item[$settings['ldap_fname_field']][0] ?? null,
|
||||||
'email' => $item[$settings['ldap_email']][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) {
|
if ($users->count() > 0) {
|
||||||
@@ -89,7 +78,7 @@ class SettingsController extends Controller
|
|||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::debug('Connection failed but we cannot debug it any further on our end.');
|
Log::debug('Connection failed but we cannot debug it any further on our end.');
|
||||||
return response()->json(['message' => $e->getMessage()], 400);
|
return response()->json(['message' => $e->getMessage()], 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -161,11 +150,8 @@ class SettingsController extends Controller
|
|||||||
if (!config('app.lock_passwords')) {
|
if (!config('app.lock_passwords')) {
|
||||||
try {
|
try {
|
||||||
Notification::send(Setting::first(), new MailTest());
|
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);
|
return response()->json(['message' => 'Mail sent to '.config('mail.reply_to.address')], 200);
|
||||||
} catch (\Exception $e) {
|
} 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);
|
return response()->json(['message' => $e->getMessage()], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,4 +315,4 @@ class SettingsController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
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\Helpers\Helper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Transformers\SelectlistTransformer;
|
use App\Http\Transformers\SelectlistTransformer;
|
||||||
@@ -31,15 +24,10 @@ class SuppliersController extends Controller
|
|||||||
public function index(Request $request): array
|
public function index(Request $request): array
|
||||||
{
|
{
|
||||||
$this->authorize('view', Supplier::class);
|
$this->authorize('view', Supplier::class);
|
||||||
$allowed_columns = [
|
$allowed_columns = ['
|
||||||
'id',
|
id',
|
||||||
'name',
|
'name',
|
||||||
'address',
|
'address',
|
||||||
'address2',
|
|
||||||
'city',
|
|
||||||
'state',
|
|
||||||
'country',
|
|
||||||
'zip',
|
|
||||||
'phone',
|
'phone',
|
||||||
'contact',
|
'contact',
|
||||||
'fax',
|
'fax',
|
||||||
@@ -51,24 +39,21 @@ class SuppliersController extends Controller
|
|||||||
'components_count',
|
'components_count',
|
||||||
'consumables_count',
|
'consumables_count',
|
||||||
'url',
|
'url',
|
||||||
'notes',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$suppliers = Supplier::select(
|
$suppliers = Supplier::select(
|
||||||
['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'created_by', 'updated_at', 'deleted_at', 'image', 'notes', 'url', 'zip'])
|
['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'updated_at', 'deleted_at', 'image', 'notes', 'url'])
|
||||||
->withCount('assets as assets_count')
|
->withCount('assets as assets_count')
|
||||||
->withCount('licenses as licenses_count')
|
->withCount('licenses as licenses_count')
|
||||||
->withCount('accessories as accessories_count')
|
->withCount('accessories as accessories_count')
|
||||||
->withCount('components as components_count')
|
->withCount('components as components_count')
|
||||||
->withCount('consumables as consumables_count')
|
->withCount('consumables as consumables_count');
|
||||||
->with('adminuser');
|
|
||||||
|
|
||||||
|
|
||||||
if ($request->filled('search')) {
|
if ($request->filled('search')) {
|
||||||
$suppliers->TextSearch($request->input('search'));
|
$suppliers = $suppliers->TextSearch($request->input('search'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if ($request->filled('name')) {
|
if ($request->filled('name')) {
|
||||||
$suppliers->where('name', '=', $request->input('name'));
|
$suppliers->where('name', '=', $request->input('name'));
|
||||||
}
|
}
|
||||||
@@ -115,15 +100,7 @@ class SuppliersController extends Controller
|
|||||||
|
|
||||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||||
|
$suppliers->orderBy($sort, $order);
|
||||||
switch ($request->input('sort')) {
|
|
||||||
case 'created_by':
|
|
||||||
$suppliers->OrderByCreatedByName($order);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$suppliers->orderBy($sort, $order);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$total = $suppliers->count();
|
$total = $suppliers->count();
|
||||||
$suppliers = $suppliers->skip($offset)->take($limit)->get();
|
$suppliers = $suppliers->skip($offset)->take($limit)->get();
|
||||||
@@ -198,40 +175,27 @@ class SuppliersController extends Controller
|
|||||||
* @since [v4.0]
|
* @since [v4.0]
|
||||||
* @param int $id
|
* @param int $id
|
||||||
*/
|
*/
|
||||||
public function destroy(Supplier $supplier): JsonResponse
|
public function destroy($id) : 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);
|
$this->authorize('delete', $supplier);
|
||||||
try {
|
|
||||||
DestroySupplierAction::run(supplier: $supplier);
|
|
||||||
} catch (ItemStillHasAssets $e) {
|
if ($supplier->assets_count > 0) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.bulk_delete_associations.assoc_assets', [
|
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count])));
|
||||||
'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')));
|
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/suppliers/message.delete.success')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,220 +0,0 @@
|
|||||||
<?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);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ use App\Helpers\Helper;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\SaveUserRequest;
|
use App\Http\Requests\SaveUserRequest;
|
||||||
use App\Http\Transformers\AccessoriesTransformer;
|
use App\Http\Transformers\AccessoriesTransformer;
|
||||||
use App\Http\Transformers\ActionlogsTransformer;
|
|
||||||
use App\Http\Transformers\AssetsTransformer;
|
use App\Http\Transformers\AssetsTransformer;
|
||||||
use App\Http\Transformers\ConsumablesTransformer;
|
use App\Http\Transformers\ConsumablesTransformer;
|
||||||
use App\Http\Transformers\LicensesTransformer;
|
use App\Http\Transformers\LicensesTransformer;
|
||||||
@@ -20,12 +19,9 @@ use App\Models\Consumable;
|
|||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Notifications\CurrentInventory;
|
use App\Notifications\CurrentInventory;
|
||||||
use App\Notifications\WelcomeNotification;
|
|
||||||
use Illuminate\Support\Facades\Artisan;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Gate;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@@ -65,14 +61,12 @@ class UsersController extends Controller
|
|||||||
'users.jobtitle',
|
'users.jobtitle',
|
||||||
'users.last_login',
|
'users.last_login',
|
||||||
'users.last_name',
|
'users.last_name',
|
||||||
'users.display_name',
|
|
||||||
'users.locale',
|
'users.locale',
|
||||||
'users.location_id',
|
'users.location_id',
|
||||||
'users.manager_id',
|
'users.manager_id',
|
||||||
'users.notes',
|
'users.notes',
|
||||||
'users.permissions',
|
'users.permissions',
|
||||||
'users.phone',
|
'users.phone',
|
||||||
'users.mobile',
|
|
||||||
'users.state',
|
'users.state',
|
||||||
'users.two_factor_enrolled',
|
'users.two_factor_enrolled',
|
||||||
'users.two_factor_optin',
|
'users.two_factor_optin',
|
||||||
@@ -86,12 +80,7 @@ class UsersController extends Controller
|
|||||||
'users.autoassign_licenses',
|
'users.autoassign_licenses',
|
||||||
'users.website',
|
'users.website',
|
||||||
|
|
||||||
])->with('manager')
|
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations')
|
||||||
->with('groups')
|
|
||||||
->with('userloc')
|
|
||||||
->with('company')
|
|
||||||
->with('department')
|
|
||||||
->with('createdBy')
|
|
||||||
->withCount([
|
->withCount([
|
||||||
'assets as assets_count' => function(Builder $query) {
|
'assets as assets_count' => function(Builder $query) {
|
||||||
$query->withoutTrashed();
|
$query->withoutTrashed();
|
||||||
@@ -103,101 +92,19 @@ class UsersController extends Controller
|
|||||||
'managedLocations as manages_locations_count'
|
'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',
|
|
||||||
|
|
||||||
// These are *relationships* so we wouldn't normally include them in this array,
|
if ($request->filled('search') != '') {
|
||||||
// since they would normally create a `column not found` error,
|
$users = $users->TextSearch($request->input('search'));
|
||||||
// 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')) {
|
if ($request->filled('activated')) {
|
||||||
$users = $users->where('users.activated', '=', $request->input('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')) {
|
if ($request->filled('company_id')) {
|
||||||
$users = $users->where('users.company_id', '=', $request->input('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')) {
|
if ($request->filled('location_id')) {
|
||||||
$users = $users->where('users.location_id', '=', $request->input('location_id'));
|
$users = $users->where('users.location_id', '=', $request->input('location_id'));
|
||||||
}
|
}
|
||||||
@@ -222,10 +129,6 @@ class UsersController extends Controller
|
|||||||
$users = $users->where('users.last_name', '=', $request->input('last_name'));
|
$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')) {
|
if ($request->filled('employee_num')) {
|
||||||
$users = $users->where('users.employee_num', '=', $request->input('employee_num'));
|
$users = $users->where('users.employee_num', '=', $request->input('employee_num'));
|
||||||
}
|
}
|
||||||
@@ -303,11 +206,11 @@ class UsersController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('manages_users_count')) {
|
if ($request->filled('manages_users_count')) {
|
||||||
$users->has('managesUsers', '=', $request->input('manages_users_count'));
|
$users->has('manages_users_count', '=', $request->input('manages_users_count'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('manages_locations_count')) {
|
if ($request->filled('manages_locations_count')) {
|
||||||
$users->has('managedLocations', '=', $request->input('manages_locations_count'));
|
$users->has('manages_locations_count', '=', $request->input('manages_locations_count'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->filled('autoassign_licenses')) {
|
if ($request->filled('autoassign_licenses')) {
|
||||||
@@ -352,6 +255,47 @@ class UsersController extends Controller
|
|||||||
$users->orderBy('first_name', $order);
|
$users->orderBy('first_name', $order);
|
||||||
break;
|
break;
|
||||||
default:
|
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';
|
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'first_name';
|
||||||
$users = $users->orderBy($sort, $order);
|
$users = $users->orderBy($sort, $order);
|
||||||
break;
|
break;
|
||||||
@@ -385,7 +329,6 @@ class UsersController extends Controller
|
|||||||
'users.employee_num',
|
'users.employee_num',
|
||||||
'users.first_name',
|
'users.first_name',
|
||||||
'users.last_name',
|
'users.last_name',
|
||||||
'users.display_name',
|
|
||||||
'users.gravatar',
|
'users.gravatar',
|
||||||
'users.avatar',
|
'users.avatar',
|
||||||
'users.email',
|
'users.email',
|
||||||
@@ -396,17 +339,19 @@ class UsersController extends Controller
|
|||||||
$users = $users->where(function ($query) use ($request) {
|
$users = $users->where(function ($query) use ($request) {
|
||||||
$query->SimpleNameSearch($request->get('search'))
|
$query->SimpleNameSearch($request->get('search'))
|
||||||
->orWhere('username', 'LIKE', '%'.$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').'%');
|
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$users = $users->orderBy('display_name', 'asc')->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
|
$users = $users->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
|
||||||
$users = $users->paginate(50);
|
$users = $users->paginate(50);
|
||||||
|
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
$name_str = $user->display_name;
|
$name_str = '';
|
||||||
|
if ($user->last_name != '') {
|
||||||
|
$name_str .= $user->last_name.', ';
|
||||||
|
}
|
||||||
|
$name_str .= $user->first_name;
|
||||||
|
|
||||||
if ($user->username != '') {
|
if ($user->username != '') {
|
||||||
$name_str .= ' ('.$user->username.')';
|
$name_str .= ' ('.$user->username.')';
|
||||||
@@ -458,20 +403,9 @@ class UsersController extends Controller
|
|||||||
$user->password = $user->noPassword();
|
$user->password = $user->noPassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
|
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
|
||||||
|
|
||||||
if ($user->save()) {
|
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')) {
|
if ($request->filled('groups')) {
|
||||||
$user->groups()->sync($request->input('groups'));
|
$user->groups()->sync($request->input('groups'));
|
||||||
} else {
|
} else {
|
||||||
@@ -539,29 +473,8 @@ class UsersController extends Controller
|
|||||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
|
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for permissions related fields and pull them out if the current user cannot edit them
|
if ($request->filled('password')) {
|
||||||
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
|
$user->password = bcrypt($request->input('password'));
|
||||||
|
|
||||||
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()
|
// We need to use has() instead of filled()
|
||||||
@@ -583,7 +496,7 @@ class UsersController extends Controller
|
|||||||
Asset::where('assigned_type', User::class)
|
Asset::where('assigned_type', User::class)
|
||||||
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
|
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
|
||||||
}
|
}
|
||||||
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
|
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
|
||||||
|
|
||||||
if ($user->save()) {
|
if ($user->save()) {
|
||||||
// Check if the request has groups passed and has a value, AND that the user us a superuser
|
// Check if the request has groups passed and has a value, AND that the user us a superuser
|
||||||
@@ -762,6 +675,7 @@ class UsersController extends Controller
|
|||||||
$this->authorize('view', License::class);
|
$this->authorize('view', License::class);
|
||||||
|
|
||||||
if ($user = User::where('id', $id)->withTrashed()->first()) {
|
if ($user = User::where('id', $id)->withTrashed()->first()) {
|
||||||
|
$this->authorize('update', $user);
|
||||||
$licenses = $user->licenses()->get();
|
$licenses = $user->licenses()->get();
|
||||||
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
|
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
|
||||||
}
|
}
|
||||||
@@ -821,25 +735,6 @@ class UsersController extends Controller
|
|||||||
return (new UsersTransformer)->transformUser($request->user());
|
return (new UsersTransformer)->transformUser($request->user());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the EULAs accepted by the user.
|
|
||||||
*
|
|
||||||
* @param \App\Models\User $user
|
|
||||||
* @param \App\Http\Transformers\ActionlogsTransformer $transformer
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*@since [v8.1.16]
|
|
||||||
* @author [Godfrey Martinez] [<gmartinez@grokability.com>]
|
|
||||||
*/
|
|
||||||
public function eulas(User $user, ActionlogsTransformer $transformer)
|
|
||||||
{
|
|
||||||
$this->authorize('view', User::class);
|
|
||||||
|
|
||||||
$eulas = $user->eulas;
|
|
||||||
return response()->json(
|
|
||||||
$transformer->transformActionlogs($eulas, $eulas->count())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore a soft-deleted user.
|
* Restore a soft-deleted user.
|
||||||
*
|
*
|
||||||
@@ -876,37 +771,4 @@ class UsersController extends Controller
|
|||||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);
|
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
272
app/Http/Controllers/AssetMaintenancesController.php
Normal file
272
app/Http/Controllers/AssetMaintenancesController.php
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\AssetMaintenance;
|
||||||
|
use App\Models\Company;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use \Illuminate\Contracts\View\View;
|
||||||
|
use \Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This controller handles all actions related to Asset Maintenance for
|
||||||
|
* the Snipe-IT Asset Management application.
|
||||||
|
*
|
||||||
|
* @version v2.0
|
||||||
|
*/
|
||||||
|
class AssetMaintenancesController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Checks for permissions for this action.
|
||||||
|
*
|
||||||
|
* @todo This should be replaced with middleware and/or policies
|
||||||
|
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||||
|
* @version v1.0
|
||||||
|
* @since [v1.8]
|
||||||
|
*/
|
||||||
|
private static function getInsufficientPermissionsRedirect(): RedirectResponse
|
||||||
|
{
|
||||||
|
return redirect()->route('maintenances.index')
|
||||||
|
->with('error', trans('general.insufficient_permissions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a view that invokes the ajax tables which actually contains
|
||||||
|
* the content for the asset maintenances listing, which is generated in getDatatable.
|
||||||
|
*
|
||||||
|
* @todo This should be replaced with middleware and/or policies
|
||||||
|
* @see AssetMaintenancesController::getDatatable() method that generates the JSON response
|
||||||
|
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||||
|
* @version v1.0
|
||||||
|
* @since [v1.8]
|
||||||
|
*/
|
||||||
|
public function index() : View
|
||||||
|
{
|
||||||
|
$this->authorize('view', Asset::class);
|
||||||
|
return view('asset_maintenances/index');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a form view to create a new asset maintenance.
|
||||||
|
*
|
||||||
|
* @see AssetMaintenancesController::postCreate() method that stores the data
|
||||||
|
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||||
|
* @version v1.0
|
||||||
|
* @since [v1.8]
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function create() : View
|
||||||
|
{
|
||||||
|
$this->authorize('update', Asset::class);
|
||||||
|
$asset = null;
|
||||||
|
|
||||||
|
if ($asset = Asset::find(request('asset_id'))) {
|
||||||
|
// We have to set this so that the correct property is set in the select2 ajax dropdown
|
||||||
|
$asset->asset_id = $asset->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare Asset Maintenance Type List
|
||||||
|
$assetMaintenanceType = [
|
||||||
|
'' => 'Select an asset maintenance type',
|
||||||
|
] + AssetMaintenance::getImprovementOptions();
|
||||||
|
// Mark the selected asset, if it came in
|
||||||
|
|
||||||
|
return view('asset_maintenances/edit')
|
||||||
|
->with('asset', $asset)
|
||||||
|
->with('assetMaintenanceType', $assetMaintenanceType)
|
||||||
|
->with('item', new AssetMaintenance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and stores the new asset maintenance
|
||||||
|
*
|
||||||
|
* @see AssetMaintenancesController::getCreate() method for the form
|
||||||
|
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||||
|
* @version v1.0
|
||||||
|
* @since [v1.8]
|
||||||
|
*/
|
||||||
|
public function store(Request $request) : RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', Asset::class);
|
||||||
|
// create a new model instance
|
||||||
|
$assetMaintenance = new AssetMaintenance();
|
||||||
|
$assetMaintenance->supplier_id = $request->input('supplier_id');
|
||||||
|
$assetMaintenance->is_warranty = $request->input('is_warranty');
|
||||||
|
$assetMaintenance->cost = $request->input('cost');
|
||||||
|
$assetMaintenance->notes = $request->input('notes');
|
||||||
|
$asset = Asset::find($request->input('asset_id'));
|
||||||
|
|
||||||
|
if ((! Company::isCurrentUserHasAccess($asset)) && ($asset != null)) {
|
||||||
|
return static::getInsufficientPermissionsRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the asset maintenance data
|
||||||
|
$assetMaintenance->asset_id = $request->input('asset_id');
|
||||||
|
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
|
||||||
|
$assetMaintenance->title = $request->input('title');
|
||||||
|
$assetMaintenance->start_date = $request->input('start_date');
|
||||||
|
$assetMaintenance->completion_date = $request->input('completion_date');
|
||||||
|
$assetMaintenance->created_by = auth()->id();
|
||||||
|
|
||||||
|
if (($assetMaintenance->completion_date !== null)
|
||||||
|
&& ($assetMaintenance->start_date !== '')
|
||||||
|
&& ($assetMaintenance->start_date !== '0000-00-00')
|
||||||
|
) {
|
||||||
|
$startDate = Carbon::parse($assetMaintenance->start_date);
|
||||||
|
$completionDate = Carbon::parse($assetMaintenance->completion_date);
|
||||||
|
$assetMaintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Was the asset maintenance created?
|
||||||
|
if ($assetMaintenance->save()) {
|
||||||
|
// Redirect to the new asset maintenance page
|
||||||
|
return redirect()->route('maintenances.index')
|
||||||
|
->with('success', trans('admin/asset_maintenances/message.create.success'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a form view to edit a selected asset maintenance.
|
||||||
|
*
|
||||||
|
* @see AssetMaintenancesController::postEdit() method that stores the data
|
||||||
|
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||||
|
* @param int $assetMaintenanceId
|
||||||
|
* @version v1.0
|
||||||
|
* @since [v1.8]
|
||||||
|
*/
|
||||||
|
public function edit(AssetMaintenance $maintenance) : View | RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', Asset::class);
|
||||||
|
if ((!$maintenance->asset) || ($maintenance->asset->deleted_at!='')) {
|
||||||
|
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
|
||||||
|
} elseif (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
||||||
|
return static::getInsufficientPermissionsRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare Improvement Type List
|
||||||
|
$assetMaintenanceType = ['' => 'Select an improvement type'] + AssetMaintenance::getImprovementOptions();
|
||||||
|
|
||||||
|
return view('asset_maintenances/edit')
|
||||||
|
->with('selectedAsset', null)
|
||||||
|
->with('assetMaintenanceType', $assetMaintenanceType)
|
||||||
|
->with('item', $maintenance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and stores an update to an asset maintenance
|
||||||
|
*
|
||||||
|
* @see AssetMaintenancesController::postEdit() method that stores the data
|
||||||
|
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||||
|
* @param Request $request
|
||||||
|
* @param int $assetMaintenanceId
|
||||||
|
* @version v1.0
|
||||||
|
* @since [v1.8]
|
||||||
|
*/
|
||||||
|
public function update(Request $request, AssetMaintenance $maintenance) : View | RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', Asset::class);
|
||||||
|
|
||||||
|
if ((!$maintenance->asset) || ($maintenance->asset->deleted_at!='')) {
|
||||||
|
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
|
||||||
|
} elseif (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
||||||
|
return static::getInsufficientPermissionsRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
$maintenance->supplier_id = $request->input('supplier_id');
|
||||||
|
$maintenance->is_warranty = $request->input('is_warranty');
|
||||||
|
$maintenance->cost = $request->input('cost');
|
||||||
|
$maintenance->notes = $request->input('notes');
|
||||||
|
|
||||||
|
$asset = Asset::find(request('asset_id'));
|
||||||
|
|
||||||
|
if (! Company::isCurrentUserHasAccess($asset)) {
|
||||||
|
return static::getInsufficientPermissionsRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the asset maintenance data
|
||||||
|
$maintenance->asset_id = $request->input('asset_id');
|
||||||
|
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
|
||||||
|
$maintenance->title = $request->input('title');
|
||||||
|
$maintenance->start_date = $request->input('start_date');
|
||||||
|
$maintenance->completion_date = $request->input('completion_date');
|
||||||
|
|
||||||
|
if (($maintenance->completion_date == null)
|
||||||
|
) {
|
||||||
|
if (($maintenance->asset_maintenance_time !== 0)
|
||||||
|
|| (! is_null($maintenance->asset_maintenance_time))
|
||||||
|
) {
|
||||||
|
$maintenance->asset_maintenance_time = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($maintenance->completion_date !== null)
|
||||||
|
&& ($maintenance->start_date !== '')
|
||||||
|
&& ($maintenance->start_date !== '0000-00-00')
|
||||||
|
) {
|
||||||
|
$startDate = Carbon::parse($maintenance->start_date);
|
||||||
|
$completionDate = Carbon::parse($maintenance->completion_date);
|
||||||
|
$maintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Was the asset maintenance created?
|
||||||
|
if ($maintenance->save()) {
|
||||||
|
|
||||||
|
// Redirect to the new asset maintenance page
|
||||||
|
return redirect()->route('maintenances.index')
|
||||||
|
->with('success', trans('admin/asset_maintenances/message.edit.success'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->withInput()->withErrors($maintenance->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an asset maintenance
|
||||||
|
*
|
||||||
|
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||||
|
* @param int $assetMaintenanceId
|
||||||
|
* @version v1.0
|
||||||
|
* @since [v1.8]
|
||||||
|
*/
|
||||||
|
public function destroy($assetMaintenanceId) : RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('update', Asset::class);
|
||||||
|
// Check if the asset maintenance exists
|
||||||
|
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
|
||||||
|
// Redirect to the asset maintenance management page
|
||||||
|
return redirect()->route('maintenances.index')
|
||||||
|
->with('error', trans('admin/asset_maintenances/message.not_found'));
|
||||||
|
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
|
||||||
|
return static::getInsufficientPermissionsRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the asset maintenance
|
||||||
|
$assetMaintenance->delete();
|
||||||
|
|
||||||
|
// Redirect to the asset_maintenance management page
|
||||||
|
return redirect()->route('maintenances.index')
|
||||||
|
->with('success', trans('admin/asset_maintenances/message.delete.success'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View an asset maintenance
|
||||||
|
*
|
||||||
|
* @author Vincent Sposato <vincent.sposato@gmail.com>
|
||||||
|
* @param int $assetMaintenanceId
|
||||||
|
* @version v1.0
|
||||||
|
* @since [v1.8]
|
||||||
|
*/
|
||||||
|
public function show(AssetMaintenance $maintenance) : View | RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('view', Asset::class);
|
||||||
|
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
||||||
|
return static::getInsufficientPermissionsRedirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('asset_maintenances/view')->with('assetMaintenance', $maintenance);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,26 +82,12 @@ class AssetModelsController extends Controller
|
|||||||
$model->notes = $request->input('notes');
|
$model->notes = $request->input('notes');
|
||||||
$model->created_by = auth()->id();
|
$model->created_by = auth()->id();
|
||||||
$model->requestable = $request->has('requestable');
|
$model->requestable = $request->has('requestable');
|
||||||
$model->require_serial = $request->input('require_serial', 0);
|
|
||||||
|
|
||||||
if ($request->input('fieldset_id') != '') {
|
if ($request->input('fieldset_id') != '') {
|
||||||
$model->fieldset_id = $request->input('fieldset_id');
|
$model->fieldset_id = $request->input('fieldset_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->has('use_cloned_image')) {
|
$model = $request->handleImages($model);
|
||||||
$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 ($model->save()) {
|
||||||
if ($this->shouldAddDefaultValues($request->input())) {
|
if ($this->shouldAddDefaultValues($request->input())) {
|
||||||
@@ -156,7 +142,7 @@ class AssetModelsController extends Controller
|
|||||||
$model->category_id = $request->input('category_id');
|
$model->category_id = $request->input('category_id');
|
||||||
$model->notes = $request->input('notes');
|
$model->notes = $request->input('notes');
|
||||||
$model->requestable = $request->input('requestable', '0');
|
$model->requestable = $request->input('requestable', '0');
|
||||||
$model->require_serial = $request->input('require_serial', 0);
|
|
||||||
$model->fieldset_id = $request->input('fieldset_id');
|
$model->fieldset_id = $request->input('fieldset_id');
|
||||||
|
|
||||||
if ($model->save()) {
|
if ($model->save()) {
|
||||||
@@ -285,7 +271,7 @@ class AssetModelsController extends Controller
|
|||||||
->with('depreciation_list', Helper::depreciationList())
|
->with('depreciation_list', Helper::depreciationList())
|
||||||
->with('item', $model)
|
->with('item', $model)
|
||||||
->with('model_id', $model->id)
|
->with('model_id', $model->id)
|
||||||
->with('cloned_model', $cloned_model);
|
->with('clone_model', $cloned_model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
115
app/Http/Controllers/AssetModelsFilesController.php
Normal file
115
app/Http/Controllers/AssetModelsFilesController.php
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?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'));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,6 @@ use Illuminate\Database\Eloquent\Builder;
|
|||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use \Illuminate\Contracts\View\View;
|
use \Illuminate\Contracts\View\View;
|
||||||
use \Illuminate\Http\RedirectResponse;
|
use \Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
|
|
||||||
class AssetCheckinController extends Controller
|
class AssetCheckinController extends Controller
|
||||||
{
|
{
|
||||||
@@ -42,19 +41,7 @@ class AssetCheckinController extends Controller
|
|||||||
return redirect()->route('hardware.show', $asset->id)->with('error', trans('admin/hardware/general.model_invalid_fix'));
|
return redirect()->route('hardware.show', $asset->id)->with('error', trans('admin/hardware/general.model_invalid_fix'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke the validation to see if the audit will complete successfully
|
return view('hardware/checkin', compact('asset'))
|
||||||
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
|
|
||||||
|
|
||||||
if ($asset->isInvalid()) {
|
|
||||||
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
|
|
||||||
}
|
|
||||||
|
|
||||||
$target_option = match ($asset->assigned_type) {
|
|
||||||
'App\Models\Asset' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.asset_previous')]),
|
|
||||||
'App\Models\Location' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.location')]),
|
|
||||||
default => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.user')]),
|
|
||||||
};
|
|
||||||
return view('hardware/checkin', compact('asset', 'target_option'))
|
|
||||||
->with('item', $asset)
|
->with('item', $asset)
|
||||||
->with('statusLabel_list', Helper::statusLabelList())
|
->with('statusLabel_list', Helper::statusLabelList())
|
||||||
->with('backto', $backto)
|
->with('backto', $backto)
|
||||||
@@ -88,14 +75,12 @@ class AssetCheckinController extends Controller
|
|||||||
|
|
||||||
$this->authorize('checkin', $asset);
|
$this->authorize('checkin', $asset);
|
||||||
|
|
||||||
session()->put('checkedInFrom', $asset->assignedTo->id);
|
if ($asset->assignedType() == Asset::USER) {
|
||||||
session()->put('checkout_to_type', match ($asset->assigned_type) {
|
$user = $asset->assignedTo;
|
||||||
'App\Models\User' => 'user',
|
}
|
||||||
'App\Models\Location' => 'location',
|
|
||||||
'App\Models\Asset' => 'asset',
|
|
||||||
});
|
|
||||||
|
|
||||||
$asset->expected_checkin = null;
|
$asset->expected_checkin = null;
|
||||||
|
$asset->last_checkin = now();
|
||||||
$asset->assignedTo()->disassociate($asset);
|
$asset->assignedTo()->disassociate($asset);
|
||||||
$asset->accepted = null;
|
$asset->accepted = null;
|
||||||
$asset->name = $request->get('name');
|
$asset->name = $request->get('name');
|
||||||
@@ -122,14 +107,11 @@ class AssetCheckinController extends Controller
|
|||||||
|
|
||||||
$originalValues = $asset->getRawOriginal();
|
$originalValues = $asset->getRawOriginal();
|
||||||
|
|
||||||
// Handle last checkin date
|
|
||||||
$checkin_at = date('Y-m-d H:i:s');
|
$checkin_at = date('Y-m-d H:i:s');
|
||||||
if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) {
|
if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) {
|
||||||
$originalValues['action_date'] = $checkin_at;
|
$originalValues['action_date'] = $checkin_at;
|
||||||
$checkin_at = $request->get('checkin_at');
|
$checkin_at = $request->get('checkin_at');
|
||||||
|
|
||||||
}
|
}
|
||||||
$asset->last_checkin = $checkin_at;
|
|
||||||
|
|
||||||
$asset->licenseseats->each(function (LicenseSeat $seat) {
|
$asset->licenseseats->each(function (LicenseSeat $seat) {
|
||||||
$seat->update(['assigned_to' => null]);
|
$seat->update(['assigned_to' => null]);
|
||||||
@@ -153,8 +135,7 @@ class AssetCheckinController extends Controller
|
|||||||
if ($asset->save()) {
|
if ($asset->save()) {
|
||||||
|
|
||||||
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues));
|
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues));
|
||||||
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))->with('success', trans('admin/hardware/message.checkin.success'));
|
||||||
->with('success', trans('admin/hardware/message.checkin.success'));
|
|
||||||
}
|
}
|
||||||
// Redirect to the asset management page with error
|
// Redirect to the asset management page with error
|
||||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.error').$asset->getErrors());
|
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.error').$asset->getErrors());
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|||||||
use Illuminate\Support\Facades\Session;
|
use Illuminate\Support\Facades\Session;
|
||||||
use \Illuminate\Contracts\View\View;
|
use \Illuminate\Contracts\View\View;
|
||||||
use \Illuminate\Http\RedirectResponse;
|
use \Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
|
|
||||||
class AssetCheckoutController extends Controller
|
class AssetCheckoutController extends Controller
|
||||||
{
|
{
|
||||||
@@ -37,14 +36,6 @@ class AssetCheckoutController extends Controller
|
|||||||
->with('error', trans('admin/hardware/general.model_invalid_fix'));
|
->with('error', trans('admin/hardware/general.model_invalid_fix'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke the validation to see if the audit will complete successfully
|
|
||||||
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
|
|
||||||
|
|
||||||
if ($asset->isInvalid()) {
|
|
||||||
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if ($asset->availableForCheckout()) {
|
if ($asset->availableForCheckout()) {
|
||||||
return view('hardware/checkout', compact('asset'))
|
return view('hardware/checkout', compact('asset'))
|
||||||
->with('statusLabel_list', Helper::deployableStatusLabelList())
|
->with('statusLabel_list', Helper::deployableStatusLabelList())
|
||||||
@@ -65,8 +56,6 @@ class AssetCheckoutController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function store(AssetCheckoutRequest $request, $assetId) : RedirectResponse
|
public function store(AssetCheckoutRequest $request, $assetId) : RedirectResponse
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if the asset exists
|
// Check if the asset exists
|
||||||
if (! $asset = Asset::find($assetId)) {
|
if (! $asset = Asset::find($assetId)) {
|
||||||
@@ -83,7 +72,6 @@ class AssetCheckoutController extends Controller
|
|||||||
$admin = auth()->user();
|
$admin = auth()->user();
|
||||||
|
|
||||||
$target = $this->determineCheckoutTarget();
|
$target = $this->determineCheckoutTarget();
|
||||||
session()->put(['checkout_to_type' => $target]);
|
|
||||||
|
|
||||||
$asset = $this->updateAssetLocation($asset, $target);
|
$asset = $this->updateAssetLocation($asset, $target);
|
||||||
|
|
||||||
@@ -126,7 +114,7 @@ class AssetCheckoutController extends Controller
|
|||||||
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
|
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'))) {
|
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->get('note'), $request->get('name'))) {
|
||||||
return Helper::getRedirectOption($request, $asset->id, 'Assets')
|
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
|
||||||
->with('success', trans('admin/hardware/message.checkout.success'));
|
->with('success', trans('admin/hardware/message.checkout.success'));
|
||||||
}
|
}
|
||||||
// Redirect to the asset management page with error
|
// Redirect to the asset management page with error
|
||||||
|
|||||||
108
app/Http/Controllers/Assets/AssetFilesController.php
Normal file
108
app/Http/Controllers/Assets/AssetFilesController.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user