Compare commits
8 Commits
v6.3.4
...
features/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a206e6c8e | ||
|
|
73acf6e5e8 | ||
|
|
9ae2d17579 | ||
|
|
3404b63922 | ||
|
|
b1796e6ff2 | ||
|
|
471527dfc6 | ||
|
|
12c24d7c0f | ||
|
|
5651d060d8 |
@@ -1,14 +1,11 @@
|
||||
{
|
||||
"projectName": "snipe-it",
|
||||
"projectOwner": "snipe",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"files": [
|
||||
"CONTRIBUTORS.md"
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 110,
|
||||
"commit": true,
|
||||
"commitConvention": "angular",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "snipe",
|
||||
@@ -2588,436 +2585,6 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "QveenSi",
|
||||
"name": "Yevhenii Huzii",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/19945501?v=4",
|
||||
"profile": "https://github.com/QveenSi",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "veenone",
|
||||
"name": "Achmad Fienan Rahardianto",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3839381?v=4",
|
||||
"profile": "https://github.com/veenone",
|
||||
"contributions": [
|
||||
"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",
|
||||
"name": "Christian Weirich",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/97299851?v=4",
|
||||
"profile": "https://github.com/chrisweirich",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "denzfarid",
|
||||
"name": "denzfarid",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1294403?v=4",
|
||||
"profile": "https://github.com/denzfarid",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "ntbutler-nbcs",
|
||||
"name": "ntbutler-nbcs",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/94018771?v=4",
|
||||
"profile": "https://github.com/ntbutler-nbcs",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "naveensrinivasan",
|
||||
"name": "Naveen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/172697?v=4",
|
||||
"profile": "https://naveensrinivasan.dev",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mikeroq",
|
||||
"name": "Mike Roquemore",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/55674383?v=4",
|
||||
"profile": "https://github.com/mikeroq",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "reederda",
|
||||
"name": "Daniel Reeder",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7991086?v=4",
|
||||
"profile": "https://github.com/reederda",
|
||||
"contributions": [
|
||||
"translation",
|
||||
"translation",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "vickyjaura183",
|
||||
"name": "vickyjaura183",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/109422491?v=4",
|
||||
"profile": "https://github.com/vickyjaura183",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "julian-piehl",
|
||||
"name": "Peace",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32363424?v=4",
|
||||
"profile": "https://github.com/julian-piehl",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kylegordon",
|
||||
"name": "Kyle Gordon",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/231528?v=4",
|
||||
"profile": "https://github.com/kylegordon",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sunflowerbofh",
|
||||
"name": "Katharina Drexel",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/53009155?v=4",
|
||||
"profile": "http://www.bfh.ch",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dsferruzza",
|
||||
"name": "David Sferruzza",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1931963?v=4",
|
||||
"profile": "https://david.sferruzza.fr/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rnelsonee",
|
||||
"name": "Rick Nelson",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/19511639?v=4",
|
||||
"profile": "https://github.com/rnelsonee",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "BasO12",
|
||||
"name": "BasO12",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/94169344?v=4",
|
||||
"profile": "https://github.com/BasO12",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Vautia",
|
||||
"name": "Vautia",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/111710123?v=4",
|
||||
"profile": "https://github.com/Vautia",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "chartjes",
|
||||
"name": "Chris Hartjes",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/28321?v=4",
|
||||
"profile": "http://www.littlehart.net/atthekeyboard",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "geo-chen",
|
||||
"name": "geo-chen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2404584?v=4",
|
||||
"profile": "https://github.com/geo-chen",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nh314",
|
||||
"name": "Phan Nguyen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6006620?v=4",
|
||||
"profile": "https://github.com/nh314",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "StarlessNights",
|
||||
"name": "Iisakki Jaakkola",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/115993812?v=4",
|
||||
"profile": "https://github.com/StarlessNights",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "eltociear",
|
||||
"name": "Ikko Ashimine",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/22633385?v=4",
|
||||
"profile": "https://bandism.net/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lukasfehling",
|
||||
"name": "Lukas Fehling",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/56871540?v=4",
|
||||
"profile": "https://github.com/lukasfehling",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "fernando-almeida",
|
||||
"name": "Fernando Almeida",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1975990?v=4",
|
||||
"profile": "https://github.com/fernando-almeida",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "akemidx",
|
||||
"name": "akemidx",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/116301219?v=4",
|
||||
"profile": "https://github.com/akemidx",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "oguzbilgic",
|
||||
"name": "Oguz Bilgic",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/144778?v=4",
|
||||
"profile": "http://oguz.site",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "scoo73r",
|
||||
"name": "Scooter Crawford",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9262438?v=4",
|
||||
"profile": "https://github.com/scoo73r",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "subdriven",
|
||||
"name": "subdriven",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5957345?v=4",
|
||||
"profile": "https://github.com/subdriven",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "AndrewSav",
|
||||
"name": "Andrew Savinykh",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/658865?v=4",
|
||||
"profile": "https://github.com/AndrewSav",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kenchan0130",
|
||||
"name": "Tadayuki Onishi",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1155067?v=4",
|
||||
"profile": "https://kenchan0130.github.io",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "floschoepfer",
|
||||
"name": "Florian",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/112496896?v=4",
|
||||
"profile": "https://github.com/floschoepfer",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "spencerrlongg",
|
||||
"name": "Spencer Long",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7305753?v=4",
|
||||
"profile": "http://spencerlong.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "marcusmoore",
|
||||
"name": "Marcus Moore",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1141514?v=4",
|
||||
"profile": "https://github.com/marcusmoore",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Mezzle",
|
||||
"name": "Martin Meredith",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/570639?v=4",
|
||||
"profile": "https://github.com/Mezzle",
|
||||
"contributions": []
|
||||
},
|
||||
{
|
||||
"login": "dboth",
|
||||
"name": "dboth",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5731963?v=4",
|
||||
"profile": "http://dboth.de",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zacharyfleck",
|
||||
"name": "Zachary Fleck",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/87536651?v=4",
|
||||
"profile": "https://github.com/zacharyfleck",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "vikaas-cyper",
|
||||
"name": "VIKAAS-A",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/74609912?v=4",
|
||||
"profile": "https://github.com/vikaas-cyper",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ak-piracha",
|
||||
"name": "Abdul Kareem",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/88882041?v=4",
|
||||
"profile": "https://github.com/ak-piracha",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "NojoudAlshehri",
|
||||
"name": "NojoudAlshehri",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/111287779?v=4",
|
||||
"profile": "https://github.com/NojoudAlshehri",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "stefanstidlffg",
|
||||
"name": "Stefan Stidl",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/54367449?v=4",
|
||||
"profile": "https://github.com/stefanstidlffg",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "qay21",
|
||||
"name": "Quentin Aymard",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/87803479?v=4",
|
||||
"profile": "https://github.com/qay21",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cram42",
|
||||
"name": "Grant Le Roux",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5396871?v=4",
|
||||
"profile": "https://github.com/cram42",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Singrity",
|
||||
"name": "Bogdan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/58479551?v=4",
|
||||
"profile": "http://@singrity",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mmanjos",
|
||||
"name": "mmanjos",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3483684?v=4",
|
||||
"profile": "https://github.com/mmanjos",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Azooz2014",
|
||||
"name": "Abdelaziz Faki",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7429229?v=4",
|
||||
"profile": "https://azooz2014.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bilias",
|
||||
"name": "bilias",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/47315739?v=4",
|
||||
"profile": "https://github.com/bilias",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "coach1988",
|
||||
"name": "coach1988",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2565989?v=4",
|
||||
"profile": "https://github.com/coach1988",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mauro-miatello",
|
||||
"name": "MrM",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/11910225?v=4",
|
||||
"profile": "https://github.com/mauro-miatello",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
12
.env.docker
12
.env.docker
@@ -138,13 +138,6 @@ PRIVATE_AWS_BUCKET=null
|
||||
PRIVATE_AWS_URL=null
|
||||
PRIVATE_AWS_BUCKET_ROOT=null
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: AWS Settings
|
||||
# --------------------------------------------
|
||||
AWS_ACCESS_KEY_ID=null
|
||||
AWS_SECRET_ACCESS_KEY=null
|
||||
AWS_DEFAULT_REGION=null
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: LOGIN THROTTLING
|
||||
# --------------------------------------------
|
||||
@@ -155,11 +148,10 @@ RESET_PASSWORD_LINK_EXPIRES=900
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: MISC
|
||||
# --------------------------------------------
|
||||
LOG_CHANNEL=stderr
|
||||
LOG_MAX_DAYS=10
|
||||
APP_LOG=stderr
|
||||
APP_LOG_MAX_FILES=10
|
||||
APP_LOCKED=false
|
||||
APP_CIPHER=AES-256-CBC
|
||||
APP_FORCE_TLS=false
|
||||
GOOGLE_MAPS_API=
|
||||
LDAP_MEM_LIM=500M
|
||||
LDAP_TIME_LIM=600
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# --------------------------------------------
|
||||
# REQUIRED: BASIC APP SETTINGS
|
||||
# --------------------------------------------
|
||||
APP_ENV=local
|
||||
APP_DEBUG=false
|
||||
APP_KEY=base64:hTUIUh9CP6dQx+6EjSlfWTgbaMaaRvlpEwk45vp+xmk=
|
||||
APP_URL=http://127.0.0.1:8000
|
||||
APP_TIMEZONE='US/Eastern'
|
||||
APP_LOCALE=en
|
||||
APP_LOCKED=false
|
||||
MAX_RESULTS=200
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: UPLOADED FILE STORAGE SETTINGS
|
||||
# --------------------------------------------
|
||||
PRIVATE_FILESYSTEM_DISK=local
|
||||
PUBLIC_FILESYSTEM_DISK=local_public
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: DATABASE SETTINGS
|
||||
# --------------------------------------------
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=null
|
||||
DB_USERNAME=null
|
||||
DB_PASSWORD=null
|
||||
DB_PREFIX=null
|
||||
#DB_DUMP_PATH=
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SSL DATABASE SETTINGS
|
||||
# --------------------------------------------
|
||||
DB_SSL=false
|
||||
DB_SSL_KEY_PATH=null
|
||||
DB_SSL_CERT_PATH=null
|
||||
DB_SSL_CA_PATH=null
|
||||
DB_SSL_CIPHER=null
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
|
||||
# --------------------------------------------
|
||||
MAIL_DRIVER="log"
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: IMAGE LIBRARY
|
||||
# This should be gd or imagick
|
||||
# --------------------------------------------
|
||||
IMAGE_LIB=gd
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SESSION SETTINGS
|
||||
# --------------------------------------------
|
||||
SESSION_LIFETIME=12000
|
||||
EXPIRE_ON_CLOSE=false
|
||||
ENCRYPT=true
|
||||
COOKIE_NAME=snipeit_v5_local
|
||||
SECURE_COOKIES=true
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SECURITY HEADER SETTINGS
|
||||
# --------------------------------------------
|
||||
REFERRER_POLICY=same-origin
|
||||
ENABLE_CSP=true
|
||||
CORS_ALLOWED_ORIGINS="*"
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: CACHE SETTINGS
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: LOGIN THROTTLING
|
||||
# --------------------------------------------
|
||||
LOGIN_MAX_ATTEMPTS=50000
|
||||
LOGIN_LOCKOUT_DURATION=1000
|
||||
RESET_PASSWORD_LINK_EXPIRES=15
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: API
|
||||
# --------------------------------------------
|
||||
API_MAX_REQUESTS_PER_HOUR=200
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SAML SETTINGS
|
||||
# --------------------------------------------
|
||||
DISABLE_NOSAML_LOCAL_LOGIN=true
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: MISC
|
||||
# --------------------------------------------
|
||||
LOG_CHANNEL=single
|
||||
LOG_LEVEL=debug
|
||||
LOG_CHANNEL=stack
|
||||
LOG_SLACK_WEBHOOK_URL=null
|
||||
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
|
||||
ALLOW_IFRAMING=true
|
||||
ENABLE_HSTS=false
|
||||
WARN_DEBUG=false
|
||||
APP_CIPHER=AES-256-CBC
|
||||
|
||||
105
.env.dusk.local
Normal file
105
.env.dusk.local
Normal file
@@ -0,0 +1,105 @@
|
||||
# --------------------------------------------
|
||||
# REQUIRED: BASIC APP SETTINGS
|
||||
# --------------------------------------------
|
||||
APP_ENV=local
|
||||
APP_DEBUG=false
|
||||
APP_KEY=base64:hTUIUh9CP6dQx+6EjSlfWTgbaMaaRvlpEwk45vp+xmk=
|
||||
APP_URL=http://127.0.0.1:8000
|
||||
APP_TIMEZONE='US/Eastern'
|
||||
APP_LOCALE=en
|
||||
APP_LOCKED=false
|
||||
MAX_RESULTS=200
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: UPLOADED FILE STORAGE SETTINGS
|
||||
# --------------------------------------------
|
||||
PRIVATE_FILESYSTEM_DISK=local
|
||||
PUBLIC_FILESYSTEM_DISK=local_public
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: DATABASE SETTINGS
|
||||
# --------------------------------------------
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=snipeit-local
|
||||
DB_USERNAME=snipeit-local
|
||||
DB_PASSWORD=snipeit-local
|
||||
DB_PREFIX=null
|
||||
DB_DUMP_PATH='/Applications/MAMP/Library/bin'
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SSL DATABASE SETTINGS
|
||||
# --------------------------------------------
|
||||
DB_SSL=false
|
||||
DB_SSL_KEY_PATH=null
|
||||
DB_SSL_CERT_PATH=null
|
||||
DB_SSL_CA_PATH=null
|
||||
DB_SSL_CIPHER=null
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
|
||||
# --------------------------------------------
|
||||
MAIL_DRIVER="log"
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: IMAGE LIBRARY
|
||||
# This should be gd or imagick
|
||||
# --------------------------------------------
|
||||
IMAGE_LIB=gd
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SESSION SETTINGS
|
||||
# --------------------------------------------
|
||||
SESSION_LIFETIME=12000
|
||||
EXPIRE_ON_CLOSE=false
|
||||
ENCRYPT=true
|
||||
COOKIE_NAME=snipeit_v5_local
|
||||
SECURE_COOKIES=true
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SECURITY HEADER SETTINGS
|
||||
# --------------------------------------------
|
||||
REFERRER_POLICY=same-origin
|
||||
ENABLE_CSP=true
|
||||
CORS_ALLOWED_ORIGINS="*"
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: CACHE SETTINGS
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: LOGIN THROTTLING
|
||||
# --------------------------------------------
|
||||
LOGIN_MAX_ATTEMPTS=50000
|
||||
LOGIN_LOCKOUT_DURATION=1000
|
||||
RESET_PASSWORD_LINK_EXPIRES=15
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: API
|
||||
# --------------------------------------------
|
||||
API_MAX_REQUESTS_PER_HOUR=200
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SAML SETTINGS
|
||||
# --------------------------------------------
|
||||
DISABLE_NOSAML_LOCAL_LOGIN=true
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: MISC
|
||||
# --------------------------------------------
|
||||
APP_LOG=single
|
||||
LOG_LEVEL=debug
|
||||
LOG_CHANNEL=stack
|
||||
LOG_SLACK_WEBHOOK_URL=null
|
||||
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
|
||||
ALLOW_IFRAMING=true
|
||||
ENABLE_HSTS=false
|
||||
WARN_DEBUG=false
|
||||
APP_CIPHER=AES-256-CBC
|
||||
|
||||
46
.env.example
46
.env.example
@@ -6,7 +6,7 @@ APP_DEBUG=false
|
||||
APP_KEY=ChangeMe
|
||||
APP_URL=null
|
||||
APP_TIMEZONE='UTC'
|
||||
APP_LOCALE='en-US'
|
||||
APP_LOCALE=en
|
||||
MAX_RESULTS=500
|
||||
|
||||
# --------------------------------------------
|
||||
@@ -24,7 +24,6 @@ PUBLIC_FILESYSTEM_DISK=local_public
|
||||
# --------------------------------------------
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=null
|
||||
DB_USERNAME=null
|
||||
DB_PASSWORD=null
|
||||
@@ -71,13 +70,11 @@ IMAGE_LIB=gd
|
||||
MAIL_BACKUP_NOTIFICATION_DRIVER=null
|
||||
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
|
||||
BACKUP_ENV=true
|
||||
ALLOW_BACKUP_DELETE=false
|
||||
ALLOW_DATA_PURGE=false
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SESSION SETTINGS
|
||||
# --------------------------------------------
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=12000
|
||||
EXPIRE_ON_CLOSE=false
|
||||
ENCRYPT=false
|
||||
@@ -85,8 +82,6 @@ COOKIE_NAME=snipeit_session
|
||||
COOKIE_DOMAIN=null
|
||||
SECURE_COOKIES=false
|
||||
API_TOKEN_EXPIRATION_YEARS=15
|
||||
BS_TABLE_STORAGE=cookieStorage
|
||||
BS_TABLE_DEEPLINK=true
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SECURITY HEADER SETTINGS
|
||||
@@ -102,6 +97,7 @@ ENABLE_HSTS=false
|
||||
# OPTIONAL: CACHE SETTINGS
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_DRIVER=sync
|
||||
CACHE_PREFIX=snipeit
|
||||
|
||||
@@ -138,32 +134,18 @@ PRIVATE_AWS_BUCKET=null
|
||||
PRIVATE_AWS_URL=null
|
||||
PRIVATE_AWS_BUCKET_ROOT=null
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: AWS Settings
|
||||
# --------------------------------------------
|
||||
AWS_ACCESS_KEY_ID=null
|
||||
AWS_SECRET_ACCESS_KEY=null
|
||||
AWS_DEFAULT_REGION=null
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: LOGIN THROTTLING
|
||||
# --------------------------------------------
|
||||
LOGIN_MAX_ATTEMPTS=5
|
||||
LOGIN_LOCKOUT_DURATION=60
|
||||
LOGIN_AUTOCOMPLETE=false
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: FORGOTTEN PASSWORD SETTINGS
|
||||
# --------------------------------------------
|
||||
RESET_PASSWORD_LINK_EXPIRES=15
|
||||
PASSWORD_CONFIRM_TIMEOUT=10800
|
||||
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
|
||||
RESET_PASSWORD_LINK_EXPIRES=900
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: MISC
|
||||
# --------------------------------------------
|
||||
LOG_CHANNEL=single
|
||||
LOG_MAX_DAYS=10
|
||||
APP_LOG=single
|
||||
APP_LOG_MAX_FILES=10
|
||||
APP_LOCKED=false
|
||||
APP_CIPHER=AES-256-CBC
|
||||
APP_FORCE_TLS=false
|
||||
@@ -176,19 +158,3 @@ IMPORT_MEMORY_LIMIT=500M
|
||||
REPORT_TIME_LIMIT=12000
|
||||
REQUIRE_SAML=false
|
||||
API_THROTTLE_PER_MINUTE=120
|
||||
CSV_ESCAPE_FORMULAS=true
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: HASHING
|
||||
# --------------------------------------------
|
||||
HASHING_DRIVER='bcrypt'
|
||||
BCRYPT_ROUNDS=10
|
||||
ARGON_MEMORY=1024
|
||||
ARGON_THREADS=2
|
||||
ARGON_TIME=2
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SCIM
|
||||
# --------------------------------------------
|
||||
SCIM_TRACE=false
|
||||
SCIM_STANDARDS_COMPLIANCE=false
|
||||
74
.env.testing
Normal file
74
.env.testing
Normal file
@@ -0,0 +1,74 @@
|
||||
# --------------------------------------------
|
||||
# REQUIRED: BASIC APP SETTINGS
|
||||
# --------------------------------------------
|
||||
APP_ENV=testing
|
||||
APP_DEBUG=true
|
||||
APP_KEY=base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU=
|
||||
APP_URL=http://localhost:8000
|
||||
APP_TIMEZONE='US/Pacific'
|
||||
APP_LOCALE=en
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: DATABASE SETTINGS
|
||||
# --------------------------------------------
|
||||
DB_CONNECTION=sqlite_testing
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=testing.sqlite
|
||||
DB_USERNAME=null
|
||||
DB_PASSWORD=null
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
|
||||
# --------------------------------------------
|
||||
MAIL_DRIVER=log
|
||||
MAIL_HOST=email-smtp.us-west-2.amazonaws.com
|
||||
MAIL_PORT=587
|
||||
MAIL_USERNAME=YOURUSERNAME
|
||||
MAIL_PASSWORD=YOURPASSWORD
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDR=you@example.com
|
||||
MAIL_FROM_NAME=Snipe-IT
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: IMAGE LIBRARY
|
||||
# This should be gd or imagick
|
||||
# --------------------------------------------
|
||||
IMAGE_LIB=gd
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: AWS S3 SETTINGS
|
||||
# --------------------------------------------
|
||||
AWS_SECRET_ACCESS_KEY=null
|
||||
AWS_ACCESS_KEY_ID=null
|
||||
AWS_DEFAULT_REGION=null
|
||||
AWS_BUCKET=null
|
||||
AWS_BUCKET_ROOT=null
|
||||
AWS_URL=null
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: CACHE SETTINGS
|
||||
# --------------------------------------------
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
QUEUE_DRIVER=sync
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: SESSION SETTINGS
|
||||
# --------------------------------------------
|
||||
SESSION_LIFETIME=12000
|
||||
EXPIRE_ON_CLOSE=false
|
||||
ENCRYPT=false
|
||||
COOKIE_NAME=snipeittest_session
|
||||
COOKIE_DOMAIN=null
|
||||
SECURE_COOKIES=false
|
||||
|
||||
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: APP LOG FORMAT
|
||||
# --------------------------------------------
|
||||
APP_LOG=single
|
||||
APP_LOG_LEVEL=debug
|
||||
@@ -6,7 +6,7 @@ APP_DEBUG=false
|
||||
APP_KEY='base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU='
|
||||
APP_URL='http://localhost:8000'
|
||||
APP_TIMEZONE='US/Pacific'
|
||||
APP_LOCALE='en-US'
|
||||
APP_LOCALE=en
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
# --------------------------------------------
|
||||
@@ -14,7 +14,6 @@ FILESYSTEM_DISK=local
|
||||
# --------------------------------------------
|
||||
DB_CONNECTION=sqlite
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE='sqlite_testing'
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=null
|
||||
@@ -35,4 +34,4 @@ IMAGE_LIB=gd
|
||||
# --------------------------------------------
|
||||
# OPTIONAL: APP LOG FORMAT
|
||||
# --------------------------------------------
|
||||
LOG_CHANNEL=single
|
||||
APP_LOG=single
|
||||
@@ -1,19 +0,0 @@
|
||||
# --------------------------------------------
|
||||
# REQUIRED: BASIC APP SETTINGS
|
||||
# --------------------------------------------
|
||||
APP_ENV=testing
|
||||
APP_DEBUG=true
|
||||
APP_KEY=base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU=
|
||||
APP_URL=http://localhost:8000
|
||||
APP_TIMEZONE='UTC'
|
||||
APP_LOCALE='en-US'
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: DATABASE SETTINGS
|
||||
# --------------------------------------------
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=null
|
||||
DB_USERNAME=null
|
||||
DB_PASSWORD=null
|
||||
@@ -4,7 +4,6 @@ APP_URL=http://snipe-it.localapp
|
||||
DB_CONNECTION=mysql
|
||||
DB_DEFAULT=mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=snipeittests
|
||||
DB_USERNAME=snipeit
|
||||
DB_PASSWORD=snipe
|
||||
|
||||
@@ -4,7 +4,6 @@ APP_URL=http://snipe-it.localapp
|
||||
DB_CONNECTION=sqlite_testing
|
||||
DB_DEFAULT=sqlite_testing
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
APP_KEY=base64:tu9NRh/a6+dCXBDGvg0Gv/0TcABnFsbT4AKxrr8mwQo=
|
||||
|
||||
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -15,8 +15,8 @@
|
||||
# *.js @octocat @github/js
|
||||
|
||||
|
||||
app/Importer/* @dmeltzer
|
||||
app/Http/Controllers/CustomFields* @uberbrady
|
||||
app/Http/Controllers/Api/CustomFields* @uberbrady
|
||||
resources/views/custom_fields/* @uberbrady
|
||||
docker/* @uberbrady
|
||||
app/Providers/SamlServiceProvider.php @uberbrady
|
||||
|
||||
67
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
67
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,25 +1,42 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature Request]: "
|
||||
labels: ["feature request"]
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. The more information you can provide about your use-case, the more liklely we are to consider your feature.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: Snipe-IT Version
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: server_operatingSystem
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: 'e.g. Ubuntu, Windows'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Web Server
|
||||
description: 'e.g. Apache, IIS'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: PHP Version
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context Add any other context or screenshots about the feature request here.
|
||||
|
||||
24
.github/autolabeler.yml
vendored
24
.github/autolabeler.yml
vendored
@@ -1,22 +1,4 @@
|
||||
frontend: ["*.js", "*.css", "*.vue", "*.scss", "*.less", "*.blade.*", "resources/views/livewire/*"]
|
||||
skins: ["*.js", "*.css", "*.scss", "*.less"]
|
||||
css: ["*.css","*.scss", "*.less"]
|
||||
javascript: ["*.js", "package.json", "package.lock"]
|
||||
backend: ["/app/*", "composer.json", "composer.lock"]
|
||||
translations: ["/resources/lang"]
|
||||
livewire: ["/app/Http/Livewire/*", "resources/views/livewire/*"]
|
||||
backups: ["*backup*"]
|
||||
restore: ["*restore*"]
|
||||
saml: ["*saml*"]
|
||||
scim: ["*scim*"]
|
||||
custom fields: ["*fields*", "*fieldsets*"]
|
||||
dependencies: ["composer.json", "composer.lock", "package.json", "package.lock"]
|
||||
consumables: ["*consumables*"]
|
||||
api: ["/app/Http/Controllers/Api/*"]
|
||||
notifications: ["/app/Notifications/*"]
|
||||
importer: ["/app/Importer/*","/app/Http/Livewire/Importer.php", "resources/views/livewire/importer.php"]
|
||||
cli / artisan: ["/app/Console/*"]
|
||||
LDAP: ["*Ldap*", "/app/Console/Commands/Ldap*","/app/Models/Ldap.php"]
|
||||
docker: ["*docker/*", "Dockerfile", "Dockerfile.alpine", "Dockerfile.fpm-alpine", ".dockerignore", ".env.docker"]
|
||||
tests: ["/tests/*", "/database/factories/*", "/stubs"]
|
||||
frontend: ["*.js", "*.css", "*.vue", "*.scss", "*.less", "*.blade.*"]
|
||||
backend: ["/app", "*.php"]
|
||||
legal: ["LICENSE*", "NOTICES*"]
|
||||
config: .github
|
||||
|
||||
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -1,7 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
target-branch: "develop"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
39
.github/workflows/SA-codeql.yml
vendored
39
.github/workflows/SA-codeql.yml
vendored
@@ -1,39 +0,0 @@
|
||||
# This workflow checks out code, performs a CodeQL analysis (for JavaScript) and integrates the results
|
||||
# with the GitHub Advanced Security code scanning feature.
|
||||
# More information: https://codeql.github.com/
|
||||
name: CodeQL Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
# schedule:
|
||||
# - cron: '15 17 * * 1'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: CodeQL Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
14
.github/workflows/codacy-analysis.yml
vendored
14
.github/workflows/codacy-analysis.yml
vendored
@@ -17,26 +17,18 @@ on:
|
||||
schedule:
|
||||
- cron: '36 23 * * 3'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codacy-security-scan:
|
||||
# Ensure schedule job never runs on forked repos. It's only executed for 'snipe/snipe-it'
|
||||
permissions:
|
||||
contents: read # for actions/checkout to fetch code
|
||||
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
|
||||
if: (github.repository == 'snipe/snipe-it') || ((github.repository != 'snipe/snipe-it') && (github.event_name != 'schedule'))
|
||||
name: Codacy Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout the repository to the GitHub Actions runner
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
|
||||
- name: Run Codacy Analysis CLI
|
||||
uses: codacy/codacy-analysis-cli-action@v4.4.0
|
||||
uses: codacy/codacy-analysis-cli-action@1.1.0
|
||||
with:
|
||||
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
|
||||
# You can also omit the token and run the tools that support default configurations
|
||||
@@ -52,6 +44,6 @@ jobs:
|
||||
|
||||
# Upload the SARIF file generated in the previous step
|
||||
- name: Upload SARIF results file
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
21
.github/workflows/crowdin-upload.yml
vendored
21
.github/workflows/crowdin-upload.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: Crowdin Action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
jobs:
|
||||
upload-sources-to-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Crowdin push
|
||||
uses: crowdin/github-action@v1
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_translations: false
|
||||
project_id: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
16
.github/workflows/docker-alpine.yml
vendored
16
.github/workflows/docker-alpine.yml
vendored
@@ -15,9 +15,6 @@ on:
|
||||
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
|
||||
@@ -32,7 +29,6 @@ jobs:
|
||||
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=tag,suffix=-alpine
|
||||
type=semver,pattern=v{{major}}-latest-alpine
|
||||
# Define default tag "flavor" for docker/metadata-action per
|
||||
# https://github.com/docker/metadata-action#flavor-input
|
||||
# We turn off 'latest' tag by default.
|
||||
@@ -42,17 +38,17 @@ jobs:
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
- name: Login to DockerHub
|
||||
# Only login if not a PR, as PRs only trigger a Docker build and not a push
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
@@ -64,7 +60,7 @@ jobs:
|
||||
# Get Metadata for docker_build step below
|
||||
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
|
||||
id: meta_build
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: snipe/snipe-it
|
||||
tags: ${{ env.IMAGE_TAGS }}
|
||||
@@ -73,11 +69,11 @@ jobs:
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push 'snipe-it' image
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.alpine
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
|
||||
# but we ONLY do an image push to DockerHub if it's NOT a PR
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
16
.github/workflows/docker.yml
vendored
16
.github/workflows/docker.yml
vendored
@@ -15,9 +15,6 @@ on:
|
||||
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
|
||||
@@ -32,7 +29,6 @@ jobs:
|
||||
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=tag
|
||||
type=semver,pattern=v{{major}}-latest
|
||||
# Define default tag "flavor" for docker/metadata-action per
|
||||
# https://github.com/docker/metadata-action#flavor-input
|
||||
# We turn off 'latest' tag by default.
|
||||
@@ -42,17 +38,17 @@ jobs:
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
- name: Login to DockerHub
|
||||
# Only login if not a PR, as PRs only trigger a Docker build and not a push
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
@@ -64,7 +60,7 @@ jobs:
|
||||
# Get Metadata for docker_build step below
|
||||
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
|
||||
id: meta_build
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: snipe/snipe-it
|
||||
tags: ${{ env.IMAGE_TAGS }}
|
||||
@@ -73,11 +69,11 @@ jobs:
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push 'snipe-it' image
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
|
||||
# but we ONLY do an image push to DockerHub if it's NOT a PR
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
22
.github/workflows/dockerhub-description.yml
vendored
22
.github/workflows/dockerhub-description.yml
vendored
@@ -1,22 +0,0 @@
|
||||
name: Update Docker Hub Description
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
paths:
|
||||
- README.md
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
jobs:
|
||||
dockerHubDescription:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Hub Description
|
||||
uses: grokability/dockerhub-description@7ea9d275c7cdbe2b676a093a0308c50665e3b8b4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
repository: snipe/snipe-it
|
||||
readme-filepath: ./README.md
|
||||
73
.github/workflows/tests-mysql.yml
vendored
73
.github/workflows/tests-mysql.yml
vendored
@@ -1,73 +0,0 @@
|
||||
name: Tests in MySQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: snipeit
|
||||
ports:
|
||||
- 33306:3306
|
||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- "7.4"
|
||||
- "8.0"
|
||||
- "8.1.1"
|
||||
|
||||
name: PHP ${{ matrix.php-version }}
|
||||
|
||||
steps:
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "${{ matrix.php-version }}"
|
||||
coverage: none
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Copy .env
|
||||
run: |
|
||||
cp -v .env.testing.example .env
|
||||
cp -v .env.testing.example .env.testing
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
|
||||
- name: Generate key
|
||||
run: php artisan key:generate
|
||||
|
||||
- name: Directory Permissions
|
||||
run: chmod -R 777 storage bootstrap/cache
|
||||
|
||||
- name: Execute tests (Unit and Feature tests) via PHPUnit
|
||||
env:
|
||||
DB_CONNECTION: mysql
|
||||
DB_DATABASE: snipeit
|
||||
DB_PORT: ${{ job.services.mysql.ports[3306] }}
|
||||
DB_USERNAME: root
|
||||
run: php artisan test --parallel
|
||||
58
.github/workflows/tests-sqlite.yml
vendored
58
.github/workflows/tests-sqlite.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: Tests in SQLite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.1.1"
|
||||
|
||||
name: PHP ${{ matrix.php-version }}
|
||||
|
||||
steps:
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: "${{ matrix.php-version }}"
|
||||
coverage: none
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Copy .env
|
||||
run: |
|
||||
cp -v .env.testing.example .env
|
||||
cp -v .env.testing.example .env.testing
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
|
||||
- name: Generate key
|
||||
run: php artisan key:generate
|
||||
|
||||
- name: Directory Permissions
|
||||
run: chmod -R 777 storage bootstrap/cache
|
||||
|
||||
- name: Execute tests (Unit and Feature tests) via PHPUnit
|
||||
env:
|
||||
DB_CONNECTION: sqlite_testing
|
||||
run: php artisan test --parallel
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,8 +1,6 @@
|
||||
.couscous
|
||||
.DS_Store
|
||||
.env
|
||||
.env.testing
|
||||
phpstan.neon
|
||||
.idea
|
||||
/bin/
|
||||
/bootstrap/compiled.php
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"DOC1": "This file is meant to be pulled from the current HEAD of the desired branch, NOT referenced locally",
|
||||
"DOC2": "In other words, what you see locally are the requirements for your _current_ install",
|
||||
"DOC3": "Please don't rely on these versions for planning upgrades unless you've fetched the most recent version",
|
||||
"DOC4": "You should really just ignore it and run upgrade.php. Really",
|
||||
"php_min_version": "7.4.0",
|
||||
"php_max_major_minor": "8.1",
|
||||
"php_max_wontwork": "8.2.0",
|
||||
"current_snipeit_version": "6.3"
|
||||
}
|
||||
444
CONTRIBUTORS.md
444
CONTRIBUTORS.md
@@ -1,444 +0,0 @@
|
||||
Thanks goes to all of these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) who have helped Snipe-IT get this far:
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.snipe.net"><img src="https://avatars3.githubusercontent.com/u/197404?v=3?s=110" width="110px;" alt="snipe"/><br /><sub><b>snipe</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Code">💻</a> <a href="#infra-snipe" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Documentation">📖</a> <a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Asnipe" title="Bug reports">🐛</a> <a href="#design-snipe" title="Design">🎨</a> <a href="https://github.com/snipe/snipe-it/pulls?q=is%3Apr+reviewed-by%3Asnipe" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.uberbrady.com"><img src="https://avatars0.githubusercontent.com/u/36335?v=3?s=110" width="110px;" alt="Brady Wetherington"/><br /><sub><b>Brady Wetherington</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=uberbrady" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=uberbrady" title="Documentation">📖</a> <a href="#infra-uberbrady" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/snipe/snipe-it/pulls?q=is%3Apr+reviewed-by%3Auberbrady" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dmeltzer"><img src="https://avatars0.githubusercontent.com/u/3803132?v=3?s=110" width="110px;" alt="Daniel Meltzer"/><br /><sub><b>Daniel Meltzer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.tuckertechonline.com"><img src="https://avatars0.githubusercontent.com/u/1609106?v=3?s=110" width="110px;" alt="Michael T"/><br /><sub><b>Michael T</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mtucker6784" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/madd15"><img src="https://avatars2.githubusercontent.com/u/3274937?v=3?s=110" width="110px;" alt="madd15"/><br /><sub><b>madd15</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=madd15" title="Documentation">📖</a> <a href="#question-madd15" title="Answering Questions">💬</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vsposato"><img src="https://avatars2.githubusercontent.com/u/894126?v=3?s=110" width="110px;" alt="Vincent Sposato"/><br /><sub><b>Vincent Sposato</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vsposato" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vjandrea"><img src="https://avatars0.githubusercontent.com/u/1639757?v=3?s=110" width="110px;" alt="Andrea Bergamasco"/><br /><sub><b>Andrea Bergamasco</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vjandrea" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kpawelski"><img src="https://avatars0.githubusercontent.com/u/10640152?v=3?s=110" width="110px;" alt="Karol"/><br /><sub><b>Karol</b></sub></a><br /><a href="#translation-kpawelski" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=kpawelski" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://blog.morph027.de/"><img src="https://avatars3.githubusercontent.com/u/600106?v=3?s=110" width="110px;" alt="morph027"/><br /><sub><b>morph027</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=morph027" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fvleminckx"><img src="https://avatars3.githubusercontent.com/u/22935755?v=3?s=110" width="110px;" alt="fvleminckx"/><br /><sub><b>fvleminckx</b></sub></a><br /><a href="#infra-fvleminckx" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itsupportcmsukorg"><img src="https://avatars2.githubusercontent.com/u/15633547?v=3?s=110" width="110px;" alt="itsupportcmsukorg"/><br /><sub><b>itsupportcmsukorg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=itsupportcmsukorg" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Aitsupportcmsukorg" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://override.io"><img src="https://avatars3.githubusercontent.com/u/12373799?v=3?s=110" width="110px;" alt="Frank"/><br /><sub><b>Frank</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=base-zero" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ghost"><img src="https://avatars0.githubusercontent.com/u/10137?v=3?s=110" width="110px;" alt="Deleted user"/><br /><sub><b>Deleted user</b></sub></a><br /><a href="#translation-ghost" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=ghost" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tiagom62"><img src="https://avatars1.githubusercontent.com/u/10802313?v=3?s=110" width="110px;" alt="tiagom62"/><br /><sub><b>tiagom62</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tiagom62" title="Code">💻</a> <a href="#infra-tiagom62" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rystaf"><img src="https://avatars3.githubusercontent.com/u/2389047?v=3?s=110" width="110px;" alt="Ryan Stafford"/><br /><sub><b>Ryan Stafford</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rystaf" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehanlon"><img src="https://avatars2.githubusercontent.com/u/10345935?v=3?s=110" width="110px;" alt="Eammon Hanlon"/><br /><sub><b>Eammon Hanlon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ehanlon" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zjean"><img src="https://avatars0.githubusercontent.com/u/441924?v=3?s=110" width="110px;" alt="zjean"/><br /><sub><b>zjean</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zjean" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.frei.media"><img src="https://avatars0.githubusercontent.com/u/12660103?v=3?s=110" width="110px;" alt="Matthias Frei"/><br /><sub><b>Matthias Frei</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FREImedia" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/opsydev"><img src="https://avatars0.githubusercontent.com/u/3767518?v=3?s=110" width="110px;" alt="opsydev"/><br /><sub><b>opsydev</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=opsydev" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.ddreier.com"><img src="https://avatars1.githubusercontent.com/u/82290?v=3?s=110" width="110px;" alt="Daniel Dreier"/><br /><sub><b>Daniel Dreier</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ddreier" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://rassie.org"><img src="https://avatars0.githubusercontent.com/u/23448?v=3?s=110" width="110px;" alt="Nikolai Prokoschenko"/><br /><sub><b>Nikolai Prokoschenko</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rassie" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YetAnotherCodeMonkey"><img src="https://avatars0.githubusercontent.com/u/13452757?v=3?s=110" width="110px;" alt="Drew"/><br /><sub><b>Drew</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=YetAnotherCodeMonkey" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/merid14"><img src="https://avatars0.githubusercontent.com/u/1342320?v=3?s=110" width="110px;" alt="Walter"/><br /><sub><b>Walter</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=merid14" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/balous"><img src="https://avatars3.githubusercontent.com/u/11254614?v=3?s=110" width="110px;" alt="Petr Baloun"/><br /><sub><b>Petr Baloun</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=balous" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reidblomquist"><img src="https://avatars0.githubusercontent.com/u/6117660?v=3?s=110" width="110px;" alt="reidblomquist"/><br /><sub><b>reidblomquist</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=reidblomquist" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mathieuk"><img src="https://avatars0.githubusercontent.com/u/539914?v=3?s=110" width="110px;" alt="Mathieu Kooiman"/><br /><sub><b>Mathieu Kooiman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mathieuk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/csayre"><img src="https://avatars3.githubusercontent.com/u/6606421?v=3?s=110" width="110px;" alt="csayre"/><br /><sub><b>csayre</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=csayre" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/adamdunson"><img src="https://avatars1.githubusercontent.com/u/768488?v=3?s=110" width="110px;" alt="Adam Dunson"/><br /><sub><b>Adam Dunson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adamdunson" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/thehereward"><img src="https://avatars0.githubusercontent.com/u/5547470?v=3?s=110" width="110px;" alt="Hereward"/><br /><sub><b>Hereward</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thehereward" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/swoopdk"><img src="https://avatars0.githubusercontent.com/u/5802977?v=3?s=110" width="110px;" alt="swoopdk"/><br /><sub><b>swoopdk</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=swoopdk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://linkedin.com/in/ahimta"><img src="https://avatars1.githubusercontent.com/u/3470403?v=3?s=110" width="110px;" alt="Abdullah Alansari"/><br /><sub><b>Abdullah Alansari</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Ahimta" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MicaelRodrigues"><img src="https://avatars0.githubusercontent.com/u/796443?v=3?s=110" width="110px;" alt="Micael Rodrigues"/><br /><sub><b>Micael Rodrigues</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=MicaelRodrigues" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://macadmincorner.com"><img src="https://avatars0.githubusercontent.com/u/614564?v=3?s=110" width="110px;" alt="Patrick Gallagher"/><br /><sub><b>Patrick Gallagher</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=patgmac" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Miliamber"><img src="https://avatars3.githubusercontent.com/u/7165922?v=3?s=110" width="110px;" alt="Miliamber"/><br /><sub><b>Miliamber</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Miliamber" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hawk554"><img src="https://avatars3.githubusercontent.com/u/861766?v=3?s=110" width="110px;" alt="hawk554"/><br /><sub><b>hawk554</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=hawk554" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://jbirdkerr.net"><img src="https://avatars1.githubusercontent.com/u/1695622?v=3?s=110" width="110px;" alt="Justin Kerr"/><br /><sub><b>Justin Kerr</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jbirdkerr" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.irasnyder.com/devel/"><img src="https://avatars3.githubusercontent.com/u/11426176?v=3?s=110" width="110px;" alt="Ira W. Snyder"/><br /><sub><b>Ira W. Snyder</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=irasnyd" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aalaily"><img src="https://avatars2.githubusercontent.com/u/2475759?v=3?s=110" width="110px;" alt="Aladin Alaily"/><br /><sub><b>Aladin Alaily</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=aalaily" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kobie-chasehansen"><img src="https://avatars0.githubusercontent.com/u/10247644?v=3?s=110" width="110px;" alt="Chase Hansen"/><br /><sub><b>Chase Hansen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kobie-chasehansen" title="Code">💻</a> <a href="#question-kobie-chasehansen" title="Answering Questions">💬</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Akobie-chasehansen" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/IDM-Helpdesk"><img src="https://avatars2.githubusercontent.com/u/13545400?v=3?s=110" width="110px;" alt="IDM Helpdesk"/><br /><sub><b>IDM Helpdesk</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=IDM-Helpdesk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://balticer.de"><img src="https://avatars2.githubusercontent.com/u/614439?v=3?s=110" width="110px;" alt="Kai"/><br /><sub><b>Kai</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=balticer" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.michaeldaniels.me"><img src="https://avatars1.githubusercontent.com/u/8762511?v=3?s=110" width="110px;" alt="Michael Daniels"/><br /><sub><b>Michael Daniels</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mdaniels5757" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://tomcastleman.me"><img src="https://avatars3.githubusercontent.com/u/1532660?v=3?s=110" width="110px;" alt="Tom Castleman"/><br /><sub><b>Tom Castleman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tomcastleman" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DanielNemanic"><img src="https://avatars3.githubusercontent.com/u/10723243?v=3?s=110" width="110px;" alt="Daniel Nemanic"/><br /><sub><b>Daniel Nemanic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DanielNemanic" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/southwolf"><img src="https://avatars0.githubusercontent.com/u/150648?v=3?s=110" width="110px;" alt="SouthWolf"/><br /><sub><b>SouthWolf</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=southwolf" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ivarne"><img src="https://avatars2.githubusercontent.com/u/131616?v=3?s=110" width="110px;" alt="Ivar Nesje"/><br /><sub><b>Ivar Nesje</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ivarne" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.j0k3r.net"><img src="https://avatars1.githubusercontent.com/u/62333?v=3?s=110" width="110px;" alt="Jérémy Benoist"/><br /><sub><b>Jérémy Benoist</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=j0k3r" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cleathley"><img src="https://avatars2.githubusercontent.com/u/724344?v=3?s=110" width="110px;" alt="Chris Leathley"/><br /><sub><b>Chris Leathley</b></sub></a><br /><a href="#infra-cleathley" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/splaer"><img src="https://avatars0.githubusercontent.com/u/972498?v=3?s=110" width="110px;" alt="splaer"/><br /><sub><b>splaer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/issues?q=author%3Asplaer" title="Bug reports">🐛</a> <a href="https://github.com/snipe/snipe-it/commits?author=splaer" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.joeferguson.me"><img src="https://avatars1.githubusercontent.com/u/967362?v=3?s=110" width="110px;" alt="Joe Ferguson"/><br /><sub><b>Joe Ferguson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=svpernova09" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/diwanicki"><img src="https://avatars3.githubusercontent.com/u/6108682?v=3?s=110" width="110px;" alt="diwanicki"/><br /><sub><b>diwanicki</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=diwanicki" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=diwanicki" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pakkua80"><img src="https://avatars3.githubusercontent.com/u/2527115?v=3?s=110" width="110px;" alt="Lee Thoong Ching"/><br /><sub><b>Lee Thoong Ching</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=pakkua80" title="Documentation">📖</a> <a href="https://github.com/snipe/snipe-it/commits?author=pakkua80" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://shu.io"><img src="https://avatars1.githubusercontent.com/u/461491?v=3?s=110" width="110px;" alt="Marek Šuppa"/><br /><sub><b>Marek Šuppa</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mrshu" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mizar1616"><img src="https://avatars1.githubusercontent.com/u/8693762?v=3?s=110" width="110px;" alt="Juan J. Martinez"/><br /><sub><b>Juan J. Martinez</b></sub></a><br /><a href="#translation-mizar1616" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rrdial"><img src="https://avatars1.githubusercontent.com/u/1458388?v=3?s=110" width="110px;" alt="R Ryan Dial"/><br /><sub><b>R Ryan Dial</b></sub></a><br /><a href="#translation-rrdial" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/burlito"><img src="https://avatars2.githubusercontent.com/u/2871745?v=3?s=110" width="110px;" alt="Andrej Manduch"/><br /><sub><b>Andrej Manduch</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=burlito" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.cordeos.com"><img src="https://avatars0.githubusercontent.com/u/8341172?v=3?s=110" width="110px;" alt="Jay Richards"/><br /><sub><b>Jay Richards</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=technogenus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://necurity.co.uk"><img src="https://avatars2.githubusercontent.com/u/7295127?v=3?s=110" width="110px;" alt="Alexander Innes"/><br /><sub><b>Alexander Innes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=leostat" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://buzzedword.codes"><img src="https://avatars2.githubusercontent.com/u/334485?v=3?s=110" width="110px;" alt="Danny Garcia"/><br /><sub><b>Danny Garcia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=buzzedword" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/archpoint"><img src="https://avatars2.githubusercontent.com/u/366855?v=3?s=110" width="110px;" alt="archpoint"/><br /><sub><b>archpoint</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=archpoint" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.jakemcgraw.com"><img src="https://avatars1.githubusercontent.com/u/67991?v=3?s=110" width="110px;" alt="Jake McGraw"/><br /><sub><b>Jake McGraw</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jakemcgraw" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FleischKarussel"><img src="https://avatars1.githubusercontent.com/u/1714374?v=3?s=110" width="110px;" alt="FleischKarussel"/><br /><sub><b>FleischKarussel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FleischKarussel" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/feeva"><img src="https://avatars3.githubusercontent.com/u/319644?v=3?s=110" width="110px;" alt="Dylan Yi"/><br /><sub><b>Dylan Yi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=feeva" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://FlashingCursor.com"><img src="https://avatars2.githubusercontent.com/u/857740?v=3?s=110" width="110px;" alt="Gil Rutkowski"/><br /><sub><b>Gil Rutkowski</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=flashingcursor" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.desmondmorris.com"><img src="https://avatars3.githubusercontent.com/u/129360?v=3?s=110" width="110px;" alt="Desmond Morris"/><br /><sub><b>Desmond Morris</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=desmondmorris" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://peelman.us"><img src="https://avatars2.githubusercontent.com/u/52936?v=3?s=110" width="110px;" alt="Nick Peelman"/><br /><sub><b>Nick Peelman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=peelman" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://abrahamvegh.com"><img src="https://avatars0.githubusercontent.com/u/53161?v=3?s=110" width="110px;" alt="Abraham Vegh"/><br /><sub><b>Abraham Vegh</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=abrahamvegh" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rashivkp"><img src="https://avatars0.githubusercontent.com/u/2818680?v=3?s=110" width="110px;" alt="Mohamed Rashid"/><br /><sub><b>Mohamed Rashid</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rashivkp" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://hinchk.github.io"><img src="https://avatars3.githubusercontent.com/u/1509456?v=3?s=110" width="110px;" alt="Kasey"/><br /><sub><b>Kasey</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=HinchK" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BrettFagerlund"><img src="https://avatars2.githubusercontent.com/u/10522541?v=3?s=110" width="110px;" alt="Brett"/><br /><sub><b>Brett</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BrettFagerlund" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://jasonspriggs.com"><img src="https://avatars2.githubusercontent.com/u/16108587?v=3?s=110" width="110px;" alt="Jason Spriggs"/><br /><sub><b>Jason Spriggs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jasonspriggs" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://n8felton.wordpress.com"><img src="https://avatars2.githubusercontent.com/u/1134568?v=3?s=110" width="110px;" alt="Nate Felton"/><br /><sub><b>Nate Felton</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=n8felton" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://homepages.dcc.ufmg.br/~manassesferreira"><img src="https://avatars2.githubusercontent.com/u/14036694?v=3?s=110" width="110px;" alt="Manasses Ferreira"/><br /><sub><b>Manasses Ferreira</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=manassesferreira" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/steveelwood"><img src="https://avatars0.githubusercontent.com/u/15913949?v=3?s=110" width="110px;" alt="Steve"/><br /><sub><b>Steve</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=steveelwood" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/matc"><img src="https://avatars1.githubusercontent.com/u/3361683?v=3?s=110" width="110px;" alt="matc"/><br /><sub><b>matc</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=matc" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.davisracingteam.com"><img src="https://avatars3.githubusercontent.com/u/7405702?v=3?s=110" width="110px;" alt="Cole R. Davis"/><br /><sub><b>Cole R. Davis</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=VanillaNinjaD" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gibsonjoshua55"><img src="https://avatars2.githubusercontent.com/u/10167681?v=3?s=110" width="110px;" alt="gibsonjoshua55"/><br /><sub><b>gibsonjoshua55</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gibsonjoshua55" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zwerch"><img src="https://avatars2.githubusercontent.com/u/2809241?v=4?s=110" width="110px;" alt="Robin Temme"/><br /><sub><b>Robin Temme</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zwerch" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/imanghafoori1"><img src="https://avatars0.githubusercontent.com/u/6961695?v=4?s=110" width="110px;" alt="Iman"/><br /><sub><b>Iman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=imanghafoori1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/richardhofman6"><img src="https://avatars1.githubusercontent.com/u/6551003?v=4?s=110" width="110px;" alt="Richard Hofman"/><br /><sub><b>Richard Hofman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=richardhofman6" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gizzmojr"><img src="https://avatars0.githubusercontent.com/u/3697569?v=4?s=110" width="110px;" alt="gizzmojr"/><br /><sub><b>gizzmojr</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gizzmojr" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/imjennyli"><img src="https://avatars3.githubusercontent.com/u/404729?v=4?s=110" width="110px;" alt="Jenny Li"/><br /><sub><b>Jenny Li</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=imjennyli" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GeoffYoung"><img src="https://avatars0.githubusercontent.com/u/869227?v=4?s=110" width="110px;" alt="Geoff Young"/><br /><sub><b>Geoff Young</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=GeoffYoung" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.elliotblackburn.com"><img src="https://avatars3.githubusercontent.com/u/1068477?v=4?s=110" width="110px;" alt="Elliot Blackburn"/><br /><sub><b>Elliot Blackburn</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BlueHatbRit" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://andmemasin.eu"><img src="https://avatars1.githubusercontent.com/u/6357451?v=4?s=110" width="110px;" alt="Tõnis Ormisson"/><br /><sub><b>Tõnis Ormisson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TonisOrmisson" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.nicolai-essig.de"><img src="https://avatars0.githubusercontent.com/u/449411?v=4?s=110" width="110px;" alt="Nicolai Essig"/><br /><sub><b>Nicolai Essig</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thakilla" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/techincolor"><img src="https://avatars1.githubusercontent.com/u/14809698?v=4?s=110" width="110px;" alt="Danielle"/><br /><sub><b>Danielle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=techincolor" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TheVakman"><img src="https://avatars1.githubusercontent.com/u/18545156?v=4?s=110" width="110px;" alt="Lawrence"/><br /><sub><b>Lawrence</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TheVakman" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3ATheVakman" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/uknzaeinozpas"><img src="https://avatars1.githubusercontent.com/u/22473767?v=4?s=110" width="110px;" alt="uknzaeinozpas"/><br /><sub><b>uknzaeinozpas</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Gelob"><img src="https://avatars3.githubusercontent.com/u/422752?v=4?s=110" width="110px;" alt="Ryan"/><br /><sub><b>Ryan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Gelob" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vcordes79"><img src="https://avatars1.githubusercontent.com/u/10672546?v=4?s=110" width="110px;" alt="vcordes79"/><br /><sub><b>vcordes79</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vcordes79" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fordster78"><img src="https://avatars3.githubusercontent.com/u/27958330?v=4?s=110" width="110px;" alt="fordster78"/><br /><sub><b>fordster78</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fordster78" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CronKz"><img src="https://avatars0.githubusercontent.com/u/34064225?v=4?s=110" width="110px;" alt="CronKz"/><br /><sub><b>CronKz</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=CronKz" title="Code">💻</a> <a href="#translation-CronKz" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tdb"><img src="https://avatars1.githubusercontent.com/u/585486?v=4?s=110" width="110px;" alt="Tim Bishop"/><br /><sub><b>Tim Bishop</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tdb" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.seanmcilvenna.com"><img src="https://avatars2.githubusercontent.com/u/5384694?v=4?s=110" width="110px;" alt="Sean McIlvenna"/><br /><sub><b>Sean McIlvenna</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=seanmcilvenna" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cepacs"><img src="https://avatars3.githubusercontent.com/u/36515590?v=4?s=110" width="110px;" alt="cepacs"/><br /><sub><b>cepacs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/issues?q=author%3Acepacs" title="Bug reports">🐛</a> <a href="https://github.com/snipe/snipe-it/commits?author=cepacs" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lea-mink"><img src="https://avatars2.githubusercontent.com/u/37537300?v=4?s=110" width="110px;" alt="lea-mink"/><br /><sub><b>lea-mink</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lea-mink" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hannahtinkler"><img src="https://avatars0.githubusercontent.com/u/7140719?v=4?s=110" width="110px;" alt="Hannah Tinkler"/><br /><sub><b>Hannah Tinkler</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=hannahtinkler" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doekman"><img src="https://avatars1.githubusercontent.com/u/1086388?v=4?s=110" width="110px;" alt="Doeke Zanstra"/><br /><sub><b>Doeke Zanstra</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=doekman" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.sdhd.nl/"><img src="https://avatars1.githubusercontent.com/u/4325936?v=4?s=110" width="110px;" alt="Djamon Staal"/><br /><sub><b>Djamon Staal</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=SjamonDaal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EarlRamirez"><img src="https://avatars3.githubusercontent.com/u/12306859?v=4?s=110" width="110px;" alt="Earl Ramirez"/><br /><sub><b>Earl Ramirez</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=EarlRamirez" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RichardRay"><img src="https://avatars2.githubusercontent.com/u/8671456?v=4?s=110" width="110px;" alt="Richard Ray Thomas"/><br /><sub><b>Richard Ray Thomas</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=RichardRay" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.taisun.io/"><img src="https://avatars3.githubusercontent.com/u/1852688?v=4?s=110" width="110px;" alt="Ryan Kuba"/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thelamer" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ParadoxGuitarist"><img src="https://avatars1.githubusercontent.com/u/6751928?v=4?s=110" width="110px;" alt="Brian Monroe"/><br /><sub><b>Brian Monroe</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ParadoxGuitarist" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/plexorama"><img src="https://avatars1.githubusercontent.com/u/605167?v=4?s=110" width="110px;" alt="plexorama"/><br /><sub><b>plexorama</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=plexorama" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://tilldeeke.de"><img src="https://avatars2.githubusercontent.com/u/1795149?v=4?s=110" width="110px;" alt="Till Deeke"/><br /><sub><b>Till Deeke</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tilldeeke" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/5quirrel"><img src="https://avatars0.githubusercontent.com/u/12634129?v=4?s=110" width="110px;" alt="5quirrel"/><br /><sub><b>5quirrel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=5quirrel" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jasonlshelton"><img src="https://avatars1.githubusercontent.com/u/13071957?v=4?s=110" width="110px;" alt="Jason"/><br /><sub><b>Jason</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jasonlshelton" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chemfy"><img src="https://avatars3.githubusercontent.com/u/7128321?v=4?s=110" width="110px;" alt="Antti"/><br /><sub><b>Antti</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chemfy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DeusMaximus"><img src="https://avatars3.githubusercontent.com/u/10080364?v=4?s=110" width="110px;" alt="DeusMaximus"/><br /><sub><b>DeusMaximus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DeusMaximus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/A-ROYAL"><img src="https://avatars2.githubusercontent.com/u/16384611?v=4?s=110" width="110px;" alt="a-royal"/><br /><sub><b>a-royal</b></sub></a><br /><a href="#translation-A-ROYAL" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/albertoaldrigo"><img src="https://avatars0.githubusercontent.com/u/5358208?v=4?s=110" width="110px;" alt="Alberto Aldrigo"/><br /><sub><b>Alberto Aldrigo</b></sub></a><br /><a href="#translation-albertoaldrigo" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://alex.stanev.org/blog"><img src="https://avatars0.githubusercontent.com/u/1412342?v=4?s=110" width="110px;" alt="Alex Stanev"/><br /><sub><b>Alex Stanev</b></sub></a><br /><a href="#translation-RealEnder" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://devel.itsolution2.de"><img src="https://avatars0.githubusercontent.com/u/177295?v=4?s=110" width="110px;" alt="Andreas Rehm"/><br /><sub><b>Andreas Rehm</b></sub></a><br /><a href="#translation-sirrus" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xelan"><img src="https://avatars0.githubusercontent.com/u/5080535?v=4?s=110" width="110px;" alt="Andreas Erhard"/><br /><sub><b>Andreas Erhard</b></sub></a><br /><a href="#translation-xelan" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/angeldeejay"><img src="https://avatars2.githubusercontent.com/u/142350?v=4?s=110" width="110px;" alt="Andrés Vanegas Jiménez"/><br /><sub><b>Andrés Vanegas Jiménez</b></sub></a><br /><a href="#translation-angeldeejay" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aschiavon91"><img src="https://avatars0.githubusercontent.com/u/3910403?v=4?s=110" width="110px;" alt="Antonio Schiavon"/><br /><sub><b>Antonio Schiavon</b></sub></a><br /><a href="#translation-aschiavon91" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benunter"><img src="https://avatars0.githubusercontent.com/u/10464547?v=4?s=110" width="110px;" alt="benunter"/><br /><sub><b>benunter</b></sub></a><br /><a href="#translation-benunter" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://catweb24.pl"><img src="https://avatars1.githubusercontent.com/u/5038647?v=4?s=110" width="110px;" alt="Borys Żmuda"/><br /><sub><b>Borys Żmuda</b></sub></a><br /><a href="#translation-rudashi" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chibacityblues"><img src="https://avatars0.githubusercontent.com/u/5539359?v=4?s=110" width="110px;" alt="chibacityblues"/><br /><sub><b>chibacityblues</b></sub></a><br /><a href="#translation-chibacityblues" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cwlin0416"><img src="https://avatars1.githubusercontent.com/u/1954830?v=4?s=110" width="110px;" alt="Chien Wei Lin"/><br /><sub><b>Chien Wei Lin</b></sub></a><br /><a href="#translation-cwlin0416" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Againstreality"><img src="https://avatars3.githubusercontent.com/u/11700533?v=4?s=110" width="110px;" alt="Christian Schuster"/><br /><sub><b>Christian Schuster</b></sub></a><br /><a href="#translation-Againstreality" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://chriss.webhostid.com"><img src="https://avatars1.githubusercontent.com/u/4308704?v=4?s=110" width="110px;" alt="Christian Stefanus"/><br /><sub><b>Christian Stefanus</b></sub></a><br /><a href="#translation-kopi-item" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://wxcafe.net"><img src="https://avatars3.githubusercontent.com/u/3009327?v=4?s=110" width="110px;" alt="wxcafé"/><br /><sub><b>wxcafé</b></sub></a><br /><a href="#translation-wxcafe" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dpyroc"><img src="https://avatars3.githubusercontent.com/u/35761525?v=4?s=110" width="110px;" alt="dpyroc"/><br /><sub><b>dpyroc</b></sub></a><br /><a href="#translation-dpyroc" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.friedlmaier.net"><img src="https://avatars1.githubusercontent.com/u/2153639?v=4?s=110" width="110px;" alt="Daniel Friedlmaier"/><br /><sub><b>Daniel Friedlmaier</b></sub></a><br /><a href="#translation-da-friedl" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/danielheene"><img src="https://avatars1.githubusercontent.com/u/2947640?v=4?s=110" width="110px;" alt="Daniel Heene"/><br /><sub><b>Daniel Heene</b></sub></a><br /><a href="#translation-danielheene" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/danielcb"><img src="https://avatars3.githubusercontent.com/u/319022?v=4?s=110" width="110px;" alt="danielcb"/><br /><sub><b>danielcb</b></sub></a><br /><a href="#translation-danielcb" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dominiksenti"><img src="https://avatars3.githubusercontent.com/u/15846537?v=4?s=110" width="110px;" alt="Dominik Senti"/><br /><sub><b>Dominik Senti</b></sub></a><br /><a href="#translation-dominiksenti" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.konectik.com"><img src="https://avatars0.githubusercontent.com/u/25570954?v=4?s=110" width="110px;" alt="Eric Gautheron"/><br /><sub><b>Eric Gautheron</b></sub></a><br /><a href="#translation-EpixFr" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://erlpil.com"><img src="https://avatars1.githubusercontent.com/u/5732623?v=4?s=110" width="110px;" alt="Erlend Pilø"/><br /><sub><b>Erlend Pilø</b></sub></a><br /><a href="#translation-Erlpil" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://fabio.technology"><img src="https://avatars0.githubusercontent.com/u/541832?v=4?s=110" width="110px;" alt="Fabio Rapposelli"/><br /><sub><b>Fabio Rapposelli</b></sub></a><br /><a href="#translation-frapposelli" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fgbs"><img src="https://avatars2.githubusercontent.com/u/3605240?v=4?s=110" width="110px;" alt="Felipe Barros"/><br /><sub><b>Felipe Barros</b></sub></a><br /><a href="#translation-fgbs" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/possebon"><img src="https://avatars0.githubusercontent.com/u/257745?v=4?s=110" width="110px;" alt="Fernando Possebon"/><br /><sub><b>Fernando Possebon</b></sub></a><br /><a href="#translation-possebon" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gdraque"><img src="https://avatars3.githubusercontent.com/u/2540832?v=4?s=110" width="110px;" alt="gdraque"/><br /><sub><b>gdraque</b></sub></a><br /><a href="#translation-gdraque" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/georgwallisch"><img src="https://avatars0.githubusercontent.com/u/23440381?v=4?s=110" width="110px;" alt="Georg Wallisch"/><br /><sub><b>Georg Wallisch</b></sub></a><br /><a href="#translation-georgwallisch" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jgroblesr85"><img src="https://avatars1.githubusercontent.com/u/9852832?v=4?s=110" width="110px;" alt="Gerardo Robles"/><br /><sub><b>Gerardo Robles</b></sub></a><br /><a href="#translation-jgroblesr85" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://t.me/Gluek"><img src="https://avatars2.githubusercontent.com/u/11082640?v=4?s=110" width="110px;" alt="Gluek"/><br /><sub><b>Gluek</b></sub></a><br /><a href="#translation-mrgluek" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AdnanAbuShahad"><img src="https://avatars0.githubusercontent.com/u/6847946?v=4?s=110" width="110px;" alt="AdnanAbuShahad"/><br /><sub><b>AdnanAbuShahad</b></sub></a><br /><a href="#translation-AdnanAbuShahad" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://hafidzi.my"><img src="https://avatars1.githubusercontent.com/u/3580608?v=4?s=110" width="110px;" alt="Hafidzi My"/><br /><sub><b>Hafidzi My</b></sub></a><br /><a href="#translation-hafidzi" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fofwisdom"><img src="https://avatars2.githubusercontent.com/u/205521?v=4?s=110" width="110px;" alt="Harim Park"/><br /><sub><b>Harim Park</b></sub></a><br /><a href="#translation-fofwisdom" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.kentsson.se"><img src="https://avatars2.githubusercontent.com/u/3333841?v=4?s=110" width="110px;" alt="Henrik Kentsson"/><br /><sub><b>Henrik Kentsson</b></sub></a><br /><a href="#translation-Kentsson" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/husnulyaqien"><img src="https://avatars0.githubusercontent.com/u/36551034?v=4?s=110" width="110px;" alt="Husnul Yaqien"/><br /><sub><b>Husnul Yaqien</b></sub></a><br /><a href="#translation-husnulyaqien" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://abaalkhail.org"><img src="https://avatars1.githubusercontent.com/u/2372747?v=4?s=110" width="110px;" alt="Ibrahim"/><br /><sub><b>Ibrahim</b></sub></a><br /><a href="#translation-abaalkh" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/igolman"><img src="https://avatars0.githubusercontent.com/u/1389334?v=4?s=110" width="110px;" alt="igolman"/><br /><sub><b>igolman</b></sub></a><br /><a href="#translation-igolman" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itangiang"><img src="https://avatars1.githubusercontent.com/u/3257070?v=4?s=110" width="110px;" alt="itangiang"/><br /><sub><b>itangiang</b></sub></a><br /><a href="#translation-itangiang" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jarby1211"><img src="https://avatars2.githubusercontent.com/u/14814254?v=4?s=110" width="110px;" alt="jarby1211"/><br /><sub><b>jarby1211</b></sub></a><br /><a href="#translation-jarby1211" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://jwillker.com"><img src="https://avatars3.githubusercontent.com/u/6719357?v=4?s=110" width="110px;" alt="Jhonn Willker"/><br /><sub><b>Jhonn Willker</b></sub></a><br /><a href="#translation-JohnWillker" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joxelito94"><img src="https://avatars2.githubusercontent.com/u/10983635?v=4?s=110" width="110px;" alt="Jose"/><br /><sub><b>Jose</b></sub></a><br /><a href="#translation-joxelito94" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/laopangzi"><img src="https://avatars0.githubusercontent.com/u/5206122?v=4?s=110" width="110px;" alt="laopangzi"/><br /><sub><b>laopangzi</b></sub></a><br /><a href="#translation-laopangzi" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://usrportage.de"><img src="https://avatars2.githubusercontent.com/u/79707?v=4?s=110" width="110px;" alt="Lars Strojny"/><br /><sub><b>Lars Strojny</b></sub></a><br /><a href="#translation-lstrojny" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/marcosbl"><img src="https://avatars0.githubusercontent.com/u/389801?v=4?s=110" width="110px;" alt="MarcosBL"/><br /><sub><b>MarcosBL</b></sub></a><br /><a href="#translation-MarcosBL" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mariejoyacajes"><img src="https://avatars3.githubusercontent.com/u/35664606?v=4?s=110" width="110px;" alt="marie joy cajes"/><br /><sub><b>marie joy cajes</b></sub></a><br /><a href="#translation-mariejoyacajes" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.markjohansen.dk"><img src="https://avatars2.githubusercontent.com/u/3052816?v=4?s=110" width="110px;" alt="Mark S. Johansen"/><br /><sub><b>Mark S. Johansen</b></sub></a><br /><a href="#translation-msjohansen" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://martinstub.dk"><img src="https://avatars2.githubusercontent.com/u/982885?v=4?s=110" width="110px;" alt="Martin Stub"/><br /><sub><b>Martin Stub</b></sub></a><br /><a href="#translation-stubben" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/meyerf99"><img src="https://avatars2.githubusercontent.com/u/28959963?v=4?s=110" width="110px;" alt="Meyer Flavio"/><br /><sub><b>Meyer Flavio</b></sub></a><br /><a href="#translation-meyerf99" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MicaelRodrigues"><img src="https://avatars3.githubusercontent.com/u/796443?v=4?s=110" width="110px;" alt="Micael Rodrigues"/><br /><sub><b>Micael Rodrigues</b></sub></a><br /><a href="#translation-MicaelRodrigues" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://rubixy.com/"><img src="https://avatars0.githubusercontent.com/u/10481331?v=4?s=110" width="110px;" alt="Mikael Rasmussen"/><br /><sub><b>Mikael Rasmussen</b></sub></a><br /><a href="#translation-mikaelssen" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/IxFail"><img src="https://avatars1.githubusercontent.com/u/1544552?v=4?s=110" width="110px;" alt="IxFail"/><br /><sub><b>IxFail</b></sub></a><br /><a href="#translation-IxFail" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.mohammedfota.com"><img src="https://avatars3.githubusercontent.com/u/18483118?v=4?s=110" width="110px;" alt="Mohammed Fota"/><br /><sub><b>Mohammed Fota</b></sub></a><br /><a href="#translation-MohammedFota" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/omego"><img src="https://avatars0.githubusercontent.com/u/227080?v=4?s=110" width="110px;" alt="Moayad Alserihi"/><br /><sub><b>Moayad Alserihi</b></sub></a><br /><a href="#translation-omego" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/saymd"><img src="https://avatars0.githubusercontent.com/u/1680266?v=4?s=110" width="110px;" alt="saymd"/><br /><sub><b>saymd</b></sub></a><br /><a href="#translation-saymd" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nordsken.se"><img src="https://avatars0.githubusercontent.com/u/1826808?v=4?s=110" width="110px;" alt="Patrik Larsson"/><br /><sub><b>Patrik Larsson</b></sub></a><br /><a href="#translation-pooot" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/drcryo"><img src="https://avatars1.githubusercontent.com/u/20584746?v=4?s=110" width="110px;" alt="drcryo"/><br /><sub><b>drcryo</b></sub></a><br /><a href="#translation-drcryo" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pawel1615"><img src="https://avatars1.githubusercontent.com/u/19408004?v=4?s=110" width="110px;" alt="pawel1615"/><br /><sub><b>pawel1615</b></sub></a><br /><a href="#translation-pawel1615" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bodrovics"><img src="https://avatars2.githubusercontent.com/u/23340468?v=4?s=110" width="110px;" alt="bodrovics"/><br /><sub><b>bodrovics</b></sub></a><br /><a href="#translation-bodrovics" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/priatna"><img src="https://avatars0.githubusercontent.com/u/3257654?v=4?s=110" width="110px;" alt="priatna"/><br /><sub><b>priatna</b></sub></a><br /><a href="#translation-priatna" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://amayume.net"><img src="https://avatars1.githubusercontent.com/u/5358374?v=4?s=110" width="110px;" alt="Fan Jiang"/><br /><sub><b>Fan Jiang</b></sub></a><br /><a href="#translation-ProfFan" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ragnarcx"><img src="https://avatars1.githubusercontent.com/u/22555451?v=4?s=110" width="110px;" alt="ragnarcx"/><br /><sub><b>ragnarcx</b></sub></a><br /><a href="#translation-ragnarcx" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.reinvanhaaren.nl/"><img src="https://avatars2.githubusercontent.com/u/18654582?v=4?s=110" width="110px;" alt="Rein van Haaren"/><br /><sub><b>Rein van Haaren</b></sub></a><br /><a href="#translation-reinvanhaaren" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://dheche.songolimo.net"><img src="https://avatars1.githubusercontent.com/u/386672?v=4?s=110" width="110px;" alt="Teguh Dwicaksana"/><br /><sub><b>Teguh Dwicaksana</b></sub></a><br /><a href="#translation-dheche" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FRaccie"><img src="https://avatars2.githubusercontent.com/u/2572552?v=4?s=110" width="110px;" alt="fraccie"/><br /><sub><b>fraccie</b></sub></a><br /><a href="#translation-FRaccie" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vinzruzell"><img src="https://avatars0.githubusercontent.com/u/35182720?v=4?s=110" width="110px;" alt="vinzruzell"/><br /><sub><b>vinzruzell</b></sub></a><br /><a href="#translation-vinzruzell" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://kevinaustin.com"><img src="https://avatars1.githubusercontent.com/u/7883603?v=4?s=110" width="110px;" alt="Kevin Austin"/><br /><sub><b>Kevin Austin</b></sub></a><br /><a href="#translation-vipsystem" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://azuraweb.xyz"><img src="https://avatars3.githubusercontent.com/u/3861828?v=4?s=110" width="110px;" alt="Wira Sandy"/><br /><sub><b>Wira Sandy</b></sub></a><br /><a href="#translation-wira-sandy" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GrayHoax"><img src="https://avatars2.githubusercontent.com/u/8663789?v=4?s=110" width="110px;" alt="Илья"/><br /><sub><b>Илья</b></sub></a><br /><a href="#translation-GrayHoax" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/godusevpn"><img src="https://avatars3.githubusercontent.com/u/30119111?v=4?s=110" width="110px;" alt="GodUseVPN"/><br /><sub><b>GodUseVPN</b></sub></a><br /><a href="#translation-godusevpn" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EngrZhou"><img src="https://avatars1.githubusercontent.com/u/745576?v=4?s=110" width="110px;" alt="周周"/><br /><sub><b>周周</b></sub></a><br /><a href="#translation-EngrZhou" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/takuy"><img src="https://avatars3.githubusercontent.com/u/1631095?v=4?s=110" width="110px;" alt="Sam"/><br /><sub><b>Sam</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=takuy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.illisian.com.au"><img src="https://avatars1.githubusercontent.com/u/264022?v=4?s=110" width="110px;" alt="Azerothian"/><br /><sub><b>Azerothian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Azerothian" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://macfoo.wordpress.com/"><img src="https://avatars1.githubusercontent.com/u/4930051?v=4?s=110" width="110px;" alt="Wes Hulette"/><br /><sub><b>Wes Hulette</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jwhulette" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/patrict"><img src="https://avatars0.githubusercontent.com/u/8134591?v=4?s=110" width="110px;" alt="patrict"/><br /><sub><b>patrict</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=patrict" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/VELIKII-DIVAN"><img src="https://avatars3.githubusercontent.com/u/2611616?v=4?s=110" width="110px;" alt="Dmitriy Minaev"/><br /><sub><b>Dmitriy Minaev</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=VELIKII-DIVAN" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liquidhorse"><img src="https://avatars0.githubusercontent.com/u/5132245?v=4?s=110" width="110px;" alt="liquidhorse"/><br /><sub><b>liquidhorse</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=liquidhorse" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://seld.be/"><img src="https://avatars1.githubusercontent.com/u/183678?v=4?s=110" width="110px;" alt="Jordi Boggiano"/><br /><sub><b>Jordi Boggiano</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Seldaek" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/inietov"><img src="https://avatars0.githubusercontent.com/u/653557?v=4?s=110" width="110px;" alt="Ivan Nieto"/><br /><sub><b>Ivan Nieto</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=inietov" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benrubson"><img src="https://avatars2.githubusercontent.com/u/6764151?v=4?s=110" width="110px;" alt="Ben RUBSON"/><br /><sub><b>Ben RUBSON</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=benrubson" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NMathar"><img src="https://avatars2.githubusercontent.com/u/8554558?v=4?s=110" width="110px;" alt="NMathar"/><br /><sub><b>NMathar</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=NMathar" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/smb"><img src="https://avatars1.githubusercontent.com/u/139566?v=4?s=110" width="110px;" alt="Steffen"/><br /><sub><b>Steffen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=smb" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Sxderp"><img src="https://avatars0.githubusercontent.com/u/6609453?v=4?s=110" width="110px;" alt="Sxderp"/><br /><sub><b>Sxderp</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Sxderp" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fanta8897"><img src="https://avatars1.githubusercontent.com/u/4807843?v=4?s=110" width="110px;" alt="fanta8897"/><br /><sub><b>fanta8897</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fanta8897" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://andreybolonin.com/phpconsulting/"><img src="https://avatars2.githubusercontent.com/u/2576509?v=4?s=110" width="110px;" alt="Andrey Bolonin"/><br /><sub><b>Andrey Bolonin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andreybolonin" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.shinayoshi.net/"><img src="https://avatars3.githubusercontent.com/u/2173307?v=4?s=110" width="110px;" alt="shinayoshi"/><br /><sub><b>shinayoshi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=shinayoshi" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reuser"><img src="https://avatars3.githubusercontent.com/u/2130159?v=4?s=110" width="110px;" alt="Hubert"/><br /><sub><b>Hubert</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=reuser" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://brashear.me"><img src="https://avatars0.githubusercontent.com/u/6865789?v=4?s=110" width="110px;" alt="KeenRivals"/><br /><sub><b>KeenRivals</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=KeenRivals" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/omyno"><img src="https://avatars3.githubusercontent.com/u/2902513?v=4?s=110" width="110px;" alt="omyno"/><br /><sub><b>omyno</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=omyno" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jackka"><img src="https://avatars1.githubusercontent.com/u/6271335?v=4?s=110" width="110px;" alt="Evgeny"/><br /><sub><b>Evgeny</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jackka" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://digitalist.se"><img src="https://avatars2.githubusercontent.com/u/1169963?v=4?s=110" width="110px;" alt="Colin Campbell"/><br /><sub><b>Colin Campbell</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=colin-campbell" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lubo"><img src="https://avatars3.githubusercontent.com/u/2872098?v=4?s=110" width="110px;" alt="Ľubomír Kučera"/><br /><sub><b>Ľubomír Kučera</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lubo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.sourceguru.net"><img src="https://avatars3.githubusercontent.com/u/570639?v=4?s=110" width="110px;" alt="Martin Meredith"/><br /><sub><b>Martin Meredith</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Mezzle" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/timothyfarmer"><img src="https://avatars1.githubusercontent.com/u/7632599?v=4?s=110" width="110px;" alt="Tim Farmer"/><br /><sub><b>Tim Farmer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=timothyfarmer" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mskrip"><img src="https://avatars0.githubusercontent.com/u/17459600?v=4?s=110" width="110px;" alt="Marián Skrip"/><br /><sub><b>Marián Skrip</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mskrip" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Godmartinz"><img src="https://avatars2.githubusercontent.com/u/47435081?v=4?s=110" width="110px;" alt="Godfrey Martinez"/><br /><sub><b>Godfrey Martinez</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Godmartinz" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bigtreeEdo"><img src="https://avatars1.githubusercontent.com/u/2075128?v=4?s=110" width="110px;" alt="bigtreeEdo"/><br /><sub><b>bigtreeEdo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bigtreeEdo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://colinmcneil.me/"><img src="https://avatars0.githubusercontent.com/u/5000430?v=4?s=110" width="110px;" alt="Colin McNeil"/><br /><sub><b>Colin McNeil</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ColinMcNeil" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JoKneeMo"><img src="https://avatars0.githubusercontent.com/u/421625?v=4?s=110" width="110px;" alt="JoKneeMo"/><br /><sub><b>JoKneeMo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JoKneeMo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.redbridge.se"><img src="https://avatars0.githubusercontent.com/u/54849013?v=4?s=110" width="110px;" alt="Joshi"/><br /><sub><b>Joshi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=joshi-redbridge" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anthonypburns"><img src="https://avatars2.githubusercontent.com/u/15731458?v=4?s=110" width="110px;" alt="Anthony Burns"/><br /><sub><b>Anthony Burns</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=anthonypburns" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/johnson-yi"><img src="https://avatars1.githubusercontent.com/u/63399474?v=4?s=110" width="110px;" alt="johnson-yi"/><br /><sub><b>johnson-yi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=johnson-yi" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://tangentmc.net"><img src="https://avatars1.githubusercontent.com/u/1862720?v=4?s=110" width="110px;" alt="Sanjay Govind"/><br /><sub><b>Sanjay Govind</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sanjay900" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://peter.upfold.org.uk/"><img src="https://avatars0.githubusercontent.com/u/1255375?v=4?s=110" width="110px;" alt="Peter Upfold"/><br /><sub><b>Peter Upfold</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PeterUpfold" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jbiel"><img src="https://avatars2.githubusercontent.com/u/961717?v=4?s=110" width="110px;" alt="Jared Biel"/><br /><sub><b>Jared Biel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jbiel" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dampfklon"><img src="https://avatars1.githubusercontent.com/u/1733625?v=4?s=110" width="110px;" alt="Dampfklon"/><br /><sub><b>Dampfklon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dampfklon" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://communityclosing.com"><img src="https://avatars2.githubusercontent.com/u/52973156?v=4?s=110" width="110px;" alt="Charles Hamilton"/><br /><sub><b>Charles Hamilton</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chamilton-ccn" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/giannello"><img src="https://avatars.githubusercontent.com/u/551789?v=4?s=110" width="110px;" alt="Giuseppe Iannello"/><br /><sub><b>Giuseppe Iannello</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=giannello" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.peterdavehello.org/"><img src="https://avatars.githubusercontent.com/u/3691490?v=4?s=110" width="110px;" alt="Peter Dave Hello"/><br /><sub><b>Peter Dave Hello</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PeterDaveHello" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sigmoidal"><img src="https://avatars.githubusercontent.com/u/6106332?v=4?s=110" width="110px;" alt="sigmoidal"/><br /><sub><b>sigmoidal</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sigmoidal" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/phenixdotnet"><img src="https://avatars.githubusercontent.com/u/2082554?v=4?s=110" width="110px;" alt="Vincent Lainé"/><br /><sub><b>Vincent Lainé</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=phenixdotnet" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.lucas-pless.com"><img src="https://avatars.githubusercontent.com/u/1943040?v=4?s=110" width="110px;" alt="Lucas Pleß"/><br /><sub><b>Lucas Pleß</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=derlucas" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/iansltx"><img src="https://avatars.githubusercontent.com/u/472804?v=4?s=110" width="110px;" alt="Ian Littman"/><br /><sub><b>Ian Littman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=iansltx" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PauloLuna"><img src="https://avatars.githubusercontent.com/u/3519029?v=4?s=110" width="110px;" alt="João Paulo"/><br /><sub><b>João Paulo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PauloLuna" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThoBur"><img src="https://avatars.githubusercontent.com/u/70443365?v=4?s=110" width="110px;" alt="ThoBur"/><br /><sub><b>ThoBur</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ThoBur" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://phpprofi.ru/"><img src="https://avatars.githubusercontent.com/u/1972329?v=4?s=110" width="110px;" alt="Alexander Chibrikin"/><br /><sub><b>Alexander Chibrikin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=alek13" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/winstan"><img src="https://avatars.githubusercontent.com/u/438332?v=4?s=110" width="110px;" alt="Anthony Winstanley"/><br /><sub><b>Anthony Winstanley</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=winstan" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fashberg"><img src="https://avatars.githubusercontent.com/u/3075214?v=4?s=110" width="110px;" alt="Folke"/><br /><sub><b>Folke</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fashberg" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benwa"><img src="https://avatars.githubusercontent.com/u/1351571?v=4?s=110" width="110px;" alt="Bennett Blodinger"/><br /><sub><b>Bennett Blodinger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=benwa" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nmc.dev"><img src="https://avatars.githubusercontent.com/u/2974631?v=4?s=110" width="110px;" alt="NMC"/><br /><sub><b>NMC</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ncareau" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andres-baller"><img src="https://avatars.githubusercontent.com/u/52182449?v=4?s=110" width="110px;" alt="andres-baller"/><br /><sub><b>andres-baller</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andres-baller" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sean-borg"><img src="https://avatars.githubusercontent.com/u/67109348?v=4?s=110" width="110px;" alt="sean-borg"/><br /><sub><b>sean-borg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sean-borg" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EDVLeer"><img src="https://avatars.githubusercontent.com/u/32170051?v=4?s=110" width="110px;" alt="EDVLeer"/><br /><sub><b>EDVLeer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=EDVLeer" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kurokat"><img src="https://avatars.githubusercontent.com/u/23075196?v=4?s=110" width="110px;" alt="Kurokat"/><br /><sub><b>Kurokat</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Kurokat" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.kevinkoellmann.de"><img src="https://avatars.githubusercontent.com/u/915514?v=4?s=110" width="110px;" alt="Kevin Köllmann"/><br /><sub><b>Kevin Köllmann</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=koelle25" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sw-mreyes"><img src="https://avatars.githubusercontent.com/u/49025941?v=4?s=110" width="110px;" alt="sw-mreyes"/><br /><sub><b>sw-mreyes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sw-mreyes" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://pittet.ca"><img src="https://avatars.githubusercontent.com/u/70129?v=4?s=110" width="110px;" alt="Joel Pittet"/><br /><sub><b>Joel Pittet</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=joelpittet" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://elyscape.com"><img src="https://avatars.githubusercontent.com/u/792695?v=4?s=110" width="110px;" alt="Eli Young"/><br /><sub><b>Eli Young</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=elyscape" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/raelldottin"><img src="https://avatars.githubusercontent.com/u/317015?v=4?s=110" width="110px;" alt="Raell Dottin"/><br /><sub><b>Raell Dottin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=raelldottin" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/misilot"><img src="https://avatars.githubusercontent.com/u/1446856?v=4?s=110" width="110px;" alt="Tom Misilo"/><br /><sub><b>Tom Misilo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=misilot" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://david.davenne.be"><img src="https://avatars.githubusercontent.com/u/4496300?v=4?s=110" width="110px;" alt="David Davenne"/><br /><sub><b>David Davenne</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JuustoMestari" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://markstenglein.com"><img src="https://avatars.githubusercontent.com/u/9255772?v=4?s=110" width="110px;" alt="Mark Stenglein"/><br /><sub><b>Mark Stenglein</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ocelotsloth" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ajsy"><img src="https://avatars.githubusercontent.com/u/35658596?v=4?s=110" width="110px;" alt="ajsy"/><br /><sub><b>ajsy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ajsy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/t3easy"><img src="https://avatars.githubusercontent.com/u/3628035?v=4?s=110" width="110px;" alt="Jan Kiesewetter"/><br /><sub><b>Jan Kiesewetter</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=t3easy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tetrachloromethane250"><img src="https://avatars.githubusercontent.com/u/79449630?v=4?s=110" width="110px;" alt="Tetrachloromethane250"/><br /><sub><b>Tetrachloromethane250</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Tetrachloromethane250" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.kajes.se/"><img src="https://avatars.githubusercontent.com/u/22004482?v=4?s=110" width="110px;" alt="Lars Kajes"/><br /><sub><b>Lars Kajes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kajes" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Joly0"><img src="https://avatars.githubusercontent.com/u/13993216?v=4?s=110" width="110px;" alt="Joly0"/><br /><sub><b>Joly0</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Joly0" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/limeless"><img src="https://avatars.githubusercontent.com/u/1501022?v=4?s=110" width="110px;" alt="theburger"/><br /><sub><b>theburger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=limeless" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/deivishome"><img src="https://avatars.githubusercontent.com/u/36065681?v=4?s=110" width="110px;" alt="David Valin Alonso"/><br /><sub><b>David Valin Alonso</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=deivishome" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andreaci"><img src="https://avatars.githubusercontent.com/u/8290389?v=4?s=110" width="110px;" alt="andreaci"/><br /><sub><b>andreaci</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andreaci" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.jellesebreghts.be"><img src="https://avatars.githubusercontent.com/u/1828542?v=4?s=110" width="110px;" alt="Jelle Sebreghts"/><br /><sub><b>Jelle Sebreghts</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Jelle-S" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Skywalker-11"><img src="https://avatars.githubusercontent.com/u/11180862?v=4?s=110" width="110px;" alt="Michael Pietsch"/><br /><sub><b>Michael Pietsch</b></sub></a><br /></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sh1hab"><img src="https://avatars.githubusercontent.com/u/22068886?v=4?s=110" width="110px;" alt="Masudul Haque Shihab"/><br /><sub><b>Masudul Haque Shihab</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sh1hab" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.freedomdive.com/"><img src="https://avatars.githubusercontent.com/u/16099942?v=4?s=110" width="110px;" alt="Supapong Areeprasertkul"/><br /><sub><b>Supapong Areeprasertkul</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zybersup" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/psarossy"><img src="https://avatars.githubusercontent.com/u/207358?v=4?s=110" width="110px;" alt="Peter Sarossy"/><br /><sub><b>Peter Sarossy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=psarossy" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nepella"><img src="https://avatars.githubusercontent.com/u/11823649?v=4?s=110" width="110px;" alt="Renee Margaret McConahy"/><br /><sub><b>Renee Margaret McConahy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nepella" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JohnnyPicnic"><img src="https://avatars.githubusercontent.com/u/5553884?v=4?s=110" width="110px;" alt="JohnnyPicnic"/><br /><sub><b>JohnnyPicnic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JohnnyPicnic" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markbrule"><img src="https://avatars.githubusercontent.com/u/8799594?v=4?s=110" width="110px;" alt="markbrule"/><br /><sub><b>markbrule</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=markbrule" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikecmpbll"><img src="https://avatars.githubusercontent.com/u/1962801?v=4?s=110" width="110px;" alt="Mike Campbell"/><br /><sub><b>Mike Campbell</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mikecmpbll" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tbrconnect"><img src="https://avatars.githubusercontent.com/u/11973217?v=4?s=110" width="110px;" alt="tbrconnect"/><br /><sub><b>tbrconnect</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tbrconnect" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kcoyo"><img src="https://avatars.githubusercontent.com/u/12447225?v=4?s=110" width="110px;" alt="kcoyo"/><br /><sub><b>kcoyo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kcoyo" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://travismiller.com/"><img src="https://avatars.githubusercontent.com/u/494017?v=4?s=110" width="110px;" alt="Travis Miller"/><br /><sub><b>Travis Miller</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=travismiller" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Delta5"><img src="https://avatars.githubusercontent.com/u/1975640?v=4?s=110" width="110px;" alt="Evan Taylor"/><br /><sub><b>Evan Taylor</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Delta5" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PetriAsi"><img src="https://avatars.githubusercontent.com/u/8735148?v=4?s=110" width="110px;" alt="Petri Asikainen"/><br /><sub><b>Petri Asikainen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PetriAsi" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/derdeagle"><img src="https://avatars.githubusercontent.com/u/11424540?v=4?s=110" width="110px;" alt="derdeagle"/><br /><sub><b>derdeagle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=derdeagle" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wh0rd.org/"><img src="https://avatars.githubusercontent.com/u/176950?v=4?s=110" width="110px;" alt="Mike Frysinger"/><br /><sub><b>Mike Frysinger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vapier" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AL4AL"><img src="https://avatars.githubusercontent.com/u/22044358?v=4?s=110" width="110px;" alt="ALPHA"/><br /><sub><b>ALPHA</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=AL4AL" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.ifern.de"><img src="https://avatars.githubusercontent.com/u/1042587?v=4?s=110" width="110px;" alt="FliegenKLATSCH"/><br /><sub><b>FliegenKLATSCH</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerm"><img src="https://avatars.githubusercontent.com/u/442138?v=4?s=110" width="110px;" alt="Jeremy Price"/><br /><sub><b>Jeremy Price</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jerm" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Toreg87"><img src="https://avatars.githubusercontent.com/u/84392209?v=4?s=110" width="110px;" alt="Toreg87"/><br /><sub><b>Toreg87</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Toreg87" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Computroniks"><img src="https://avatars.githubusercontent.com/u/67638596?v=4?s=110" width="110px;" alt="Matthew Nickson"/><br /><sub><b>Matthew Nickson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Computroniks" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://jethron.id.au"><img src="https://avatars.githubusercontent.com/u/1646397?v=4?s=110" width="110px;" alt="Jethro Nederhof"/><br /><sub><b>Jethro Nederhof</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jethron" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/01ste02"><img src="https://avatars.githubusercontent.com/u/23289826?v=4?s=110" width="110px;" alt="Oskar Stenberg"/><br /><sub><b>Oskar Stenberg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=01ste02" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Robert-Azelis"><img src="https://avatars.githubusercontent.com/u/82208283?v=4?s=110" width="110px;" alt="Robert-Azelis"/><br /><sub><b>Robert-Azelis</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Robert-Azelis" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alwism"><img src="https://avatars.githubusercontent.com/u/60648387?v=4?s=110" width="110px;" alt="Alexander William Smith"/><br /><sub><b>Alexander William Smith</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=alwism" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.leitwerk.de/"><img src="https://avatars.githubusercontent.com/u/24418301?v=4?s=110" width="110px;" alt="LEITWERK AG"/><br /><sub><b>LEITWERK AG</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=leitwerk-ag" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.aboutcher.co.uk"><img src="https://avatars.githubusercontent.com/u/1911435?v=4?s=110" width="110px;" alt="Adam"/><br /><sub><b>Adam</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adamboutcher" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://snksrv.com"><img src="https://avatars.githubusercontent.com/u/16104273?v=4?s=110" width="110px;" alt="Ian"/><br /><sub><b>Ian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sneak-it" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://blog.bestlong.idv.tw/"><img src="https://avatars.githubusercontent.com/u/4023909?v=4?s=110" width="110px;" alt="Shao Yu-Lung (Allen)"/><br /><sub><b>Shao Yu-Lung (Allen)</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bestlong" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Haxatron"><img src="https://avatars.githubusercontent.com/u/76475453?v=4?s=110" width="110px;" alt="Haxatron"/><br /><sub><b>Haxatron</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Haxatron" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PlaneNuts"><img src="https://avatars.githubusercontent.com/u/88776392?v=4?s=110" width="110px;" alt="PlaneNuts"/><br /><sub><b>PlaneNuts</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PlaneNuts" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://bjcpgd.cias.rit.edu"><img src="https://avatars.githubusercontent.com/u/3842948?v=4?s=110" width="110px;" alt="Bradley Coudriet"/><br /><sub><b>Bradley Coudriet</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=exula" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://daltondur.st"><img src="https://avatars.githubusercontent.com/u/21966173?v=4?s=110" width="110px;" alt="Dalton Durst"/><br /><sub><b>Dalton Durst</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=UniversalSuperBox" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://adagiohealth.org"><img src="https://avatars.githubusercontent.com/u/38761237?v=4?s=110" width="110px;" alt="Alex Janes"/><br /><sub><b>Alex Janes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adagioajanes" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nuraeil"><img src="https://avatars.githubusercontent.com/u/32387849?v=4?s=110" width="110px;" alt="Nuraeil"/><br /><sub><b>Nuraeil</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nuraeil" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TenOfTens"><img src="https://avatars.githubusercontent.com/u/48162670?v=4?s=110" width="110px;" alt="TenOfTens"/><br /><sub><b>TenOfTens</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TenOfTens" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://ditisjens.be/"><img src="https://avatars.githubusercontent.com/u/9415391?v=4?s=110" width="110px;" alt="waffle"/><br /><sub><b>waffle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=insert-waffle" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QveenSi"><img src="https://avatars.githubusercontent.com/u/19945501?v=4?s=110" width="110px;" alt="Yevhenii Huzii"/><br /><sub><b>Yevhenii Huzii</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=QveenSi" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/veenone"><img src="https://avatars.githubusercontent.com/u/3839381?v=4?s=110" width="110px;" alt="Achmad Fienan Rahardianto"/><br /><sub><b>Achmad Fienan Rahardianto</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=veenone" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QveenSi"><img src="https://avatars.githubusercontent.com/u/19945501?v=4?s=110" width="110px;" alt="Yevhenii Huzii"/><br /><sub><b>Yevhenii Huzii</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=QveenSi" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chrisweirich"><img src="https://avatars.githubusercontent.com/u/97299851?v=4?s=110" width="110px;" alt="Christian Weirich"/><br /><sub><b>Christian Weirich</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chrisweirich" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/denzfarid"><img src="https://avatars.githubusercontent.com/u/1294403?v=4?s=110" width="110px;" alt="denzfarid"/><br /><sub><b>denzfarid</b></sub></a><br /></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ntbutler-nbcs"><img src="https://avatars.githubusercontent.com/u/94018771?v=4?s=110" width="110px;" alt="ntbutler-nbcs"/><br /><sub><b>ntbutler-nbcs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://naveensrinivasan.dev"><img src="https://avatars.githubusercontent.com/u/172697?v=4?s=110" width="110px;" alt="Naveen"/><br /><sub><b>Naveen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=naveensrinivasan" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikeroq"><img src="https://avatars.githubusercontent.com/u/55674383?v=4?s=110" width="110px;" alt="Mike Roquemore"/><br /><sub><b>Mike Roquemore</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mikeroq" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reederda"><img src="https://avatars.githubusercontent.com/u/7991086?v=4?s=110" width="110px;" alt="Daniel Reeder"/><br /><sub><b>Daniel Reeder</b></sub></a><br /><a href="#translation-reederda" title="Translation">🌍</a> <a href="#translation-reederda" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=reederda" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vickyjaura183"><img src="https://avatars.githubusercontent.com/u/109422491?v=4?s=110" width="110px;" alt="vickyjaura183"/><br /><sub><b>vickyjaura183</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vickyjaura183" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/julian-piehl"><img src="https://avatars.githubusercontent.com/u/32363424?v=4?s=110" width="110px;" alt="Peace"/><br /><sub><b>Peace</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=julian-piehl" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kylegordon"><img src="https://avatars.githubusercontent.com/u/231528?v=4?s=110" width="110px;" alt="Kyle Gordon"/><br /><sub><b>Kyle Gordon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kylegordon" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.bfh.ch"><img src="https://avatars.githubusercontent.com/u/53009155?v=4?s=110" width="110px;" alt="Katharina Drexel"/><br /><sub><b>Katharina Drexel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sunflowerbofh" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://david.sferruzza.fr/"><img src="https://avatars.githubusercontent.com/u/1931963?v=4?s=110" width="110px;" alt="David Sferruzza"/><br /><sub><b>David Sferruzza</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dsferruzza" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rnelsonee"><img src="https://avatars.githubusercontent.com/u/19511639?v=4?s=110" width="110px;" alt="Rick Nelson"/><br /><sub><b>Rick Nelson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rnelsonee" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BasO12"><img src="https://avatars.githubusercontent.com/u/94169344?v=4?s=110" width="110px;" alt="BasO12"/><br /><sub><b>BasO12</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BasO12" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Vautia"><img src="https://avatars.githubusercontent.com/u/111710123?v=4?s=110" width="110px;" alt="Vautia"/><br /><sub><b>Vautia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Vautia" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.littlehart.net/atthekeyboard"><img src="https://avatars.githubusercontent.com/u/28321?v=4?s=110" width="110px;" alt="Chris Hartjes"/><br /><sub><b>Chris Hartjes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chartjes" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geo-chen"><img src="https://avatars.githubusercontent.com/u/2404584?v=4?s=110" width="110px;" alt="geo-chen"/><br /><sub><b>geo-chen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=geo-chen" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nh314"><img src="https://avatars.githubusercontent.com/u/6006620?v=4?s=110" width="110px;" alt="Phan Nguyen"/><br /><sub><b>Phan Nguyen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nh314" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/StarlessNights"><img src="https://avatars.githubusercontent.com/u/115993812?v=4?s=110" width="110px;" alt="Iisakki Jaakkola"/><br /><sub><b>Iisakki Jaakkola</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=StarlessNights" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=110" width="110px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=eltociear" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lukasfehling"><img src="https://avatars.githubusercontent.com/u/56871540?v=4?s=110" width="110px;" alt="Lukas Fehling"/><br /><sub><b>Lukas Fehling</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lukasfehling" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fernando-almeida"><img src="https://avatars.githubusercontent.com/u/1975990?v=4?s=110" width="110px;" alt="Fernando Almeida"/><br /><sub><b>Fernando Almeida</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fernando-almeida" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/akemidx"><img src="https://avatars.githubusercontent.com/u/116301219?v=4?s=110" width="110px;" alt="akemidx"/><br /><sub><b>akemidx</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=akemidx" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://oguz.site"><img src="https://avatars.githubusercontent.com/u/144778?v=4?s=110" width="110px;" alt="Oguz Bilgic"/><br /><sub><b>Oguz Bilgic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=oguzbilgic" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/scoo73r"><img src="https://avatars.githubusercontent.com/u/9262438?v=4?s=110" width="110px;" alt="Scooter Crawford"/><br /><sub><b>Scooter Crawford</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=scoo73r" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/subdriven"><img src="https://avatars.githubusercontent.com/u/5957345?v=4?s=110" width="110px;" alt="subdriven"/><br /><sub><b>subdriven</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=subdriven" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AndrewSav"><img src="https://avatars.githubusercontent.com/u/658865?v=4?s=110" width="110px;" alt="Andrew Savinykh"/><br /><sub><b>Andrew Savinykh</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=AndrewSav" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://kenchan0130.github.io"><img src="https://avatars.githubusercontent.com/u/1155067?v=4?s=110" width="110px;" alt="Tadayuki Onishi"/><br /><sub><b>Tadayuki Onishi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kenchan0130" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/floschoepfer"><img src="https://avatars.githubusercontent.com/u/112496896?v=4?s=110" width="110px;" alt="Florian"/><br /><sub><b>Florian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=floschoepfer" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://spencerlong.com"><img src="https://avatars.githubusercontent.com/u/7305753?v=4?s=110" width="110px;" alt="Spencer Long"/><br /><sub><b>Spencer Long</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=spencerrlongg" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marcusmoore"><img src="https://avatars.githubusercontent.com/u/1141514?v=4?s=110" width="110px;" alt="Marcus Moore"/><br /><sub><b>Marcus Moore</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=marcusmoore" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mezzle"><img src="https://avatars.githubusercontent.com/u/570639?v=4?s=110" width="110px;" alt="Martin Meredith"/><br /><sub><b>Martin Meredith</b></sub></a><br /></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://dboth.de"><img src="https://avatars.githubusercontent.com/u/5731963?v=4?s=110" width="110px;" alt="dboth"/><br /><sub><b>dboth</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dboth" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zacharyfleck"><img src="https://avatars.githubusercontent.com/u/87536651?v=4?s=110" width="110px;" alt="Zachary Fleck"/><br /><sub><b>Zachary Fleck</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zacharyfleck" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vikaas-cyper"><img src="https://avatars.githubusercontent.com/u/74609912?v=4?s=110" width="110px;" alt="VIKAAS-A"/><br /><sub><b>VIKAAS-A</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vikaas-cyper" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ak-piracha"><img src="https://avatars.githubusercontent.com/u/88882041?v=4?s=110" width="110px;" alt="Abdul Kareem"/><br /><sub><b>Abdul Kareem</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ak-piracha" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NojoudAlshehri"><img src="https://avatars.githubusercontent.com/u/111287779?v=4?s=110" width="110px;" alt="NojoudAlshehri"/><br /><sub><b>NojoudAlshehri</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stefanstidlffg"><img src="https://avatars.githubusercontent.com/u/54367449?v=4?s=110" width="110px;" alt="Stefan Stidl"/><br /><sub><b>Stefan Stidl</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=stefanstidlffg" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qay21"><img src="https://avatars.githubusercontent.com/u/87803479?v=4?s=110" width="110px;" alt="Quentin Aymard"/><br /><sub><b>Quentin Aymard</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=qay21" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cram42"><img src="https://avatars.githubusercontent.com/u/5396871?v=4?s=110" width="110px;" alt="Grant Le Roux"/><br /><sub><b>Grant Le Roux</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=cram42" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://@singrity"><img src="https://avatars.githubusercontent.com/u/58479551?v=4?s=110" width="110px;" alt="Bogdan"/><br /><sub><b>Bogdan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Singrity" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mmanjos"><img src="https://avatars.githubusercontent.com/u/3483684?v=4?s=110" width="110px;" alt="mmanjos"/><br /><sub><b>mmanjos</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mmanjos" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://azooz2014.github.io/"><img src="https://avatars.githubusercontent.com/u/7429229?v=4?s=110" width="110px;" alt="Abdelaziz Faki"/><br /><sub><b>Abdelaziz Faki</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Azooz2014" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bilias"><img src="https://avatars.githubusercontent.com/u/47315739?v=4?s=110" width="110px;" alt="bilias"/><br /><sub><b>bilias</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bilias" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/coach1988"><img src="https://avatars.githubusercontent.com/u/2565989?v=4?s=110" width="110px;" alt="coach1988"/><br /><sub><b>coach1988</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=coach1988" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mauro-miatello"><img src="https://avatars.githubusercontent.com/u/11910225?v=4?s=110" width="110px;" alt="MrM"/><br /><sub><b>MrM</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mauro-miatello" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
|
||||
35
Dockerfile
35
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:22.04
|
||||
FROM ubuntu:20.04
|
||||
LABEL maintainer="Brady Wetherington <bwetherington@grokability.com>"
|
||||
|
||||
# No need to add `apt-get clean` here, reference:
|
||||
@@ -14,17 +14,15 @@ RUN export DEBIAN_FRONTEND=noninteractive; \
|
||||
apt-utils \
|
||||
apache2 \
|
||||
apache2-bin \
|
||||
libapache2-mod-php8.1 \
|
||||
php8.1-curl \
|
||||
php8.1-ldap \
|
||||
php8.1-mysql \
|
||||
php8.1-gd \
|
||||
php8.1-xml \
|
||||
php8.1-mbstring \
|
||||
php8.1-zip \
|
||||
php8.1-bcmath \
|
||||
php8.1-redis \
|
||||
php-memcached \
|
||||
libapache2-mod-php7.4 \
|
||||
php7.4-curl \
|
||||
php7.4-ldap \
|
||||
php7.4-mysql \
|
||||
php7.4-gd \
|
||||
php7.4-xml \
|
||||
php7.4-mbstring \
|
||||
php7.4-zip \
|
||||
php7.4-bcmath \
|
||||
patch \
|
||||
curl \
|
||||
wget \
|
||||
@@ -38,10 +36,9 @@ gcc \
|
||||
make \
|
||||
autoconf \
|
||||
libc-dev \
|
||||
libldap-common \
|
||||
pkg-config \
|
||||
libmcrypt-dev \
|
||||
php8.1-dev \
|
||||
php7.4-dev \
|
||||
ca-certificates \
|
||||
unzip \
|
||||
dnsutils \
|
||||
@@ -51,16 +48,16 @@ dnsutils \
|
||||
RUN curl -L -O https://github.com/pear/pearweb_phars/raw/master/go-pear.phar
|
||||
RUN php go-pear.phar
|
||||
|
||||
RUN pecl install mcrypt
|
||||
RUN pecl install mcrypt-1.0.3
|
||||
|
||||
RUN bash -c "echo extension=/usr/lib/php/20210902/mcrypt.so > /etc/php/8.1/mods-available/mcrypt.ini"
|
||||
RUN bash -c "echo extension=/usr/lib/php/20190902/mcrypt.so > /etc/php/7.4/mods-available/mcrypt.ini"
|
||||
|
||||
RUN phpenmod mcrypt
|
||||
RUN phpenmod gd
|
||||
RUN phpenmod bcmath
|
||||
|
||||
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.1/apache2/php.ini
|
||||
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.1/cli/php.ini
|
||||
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/7.4/apache2/php.ini
|
||||
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/7.4/cli/php.ini
|
||||
|
||||
RUN useradd -m --uid 1000 --gid 50 docker
|
||||
|
||||
@@ -141,4 +138,4 @@ RUN chmod +x /startup.sh /usr/bin/supervisor-exit-event-listener
|
||||
CMD ["/startup.sh"]
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
EXPOSE 443
|
||||
@@ -1,35 +1,32 @@
|
||||
FROM alpine:3.18.6
|
||||
FROM alpine:3.14.2
|
||||
# Apache + PHP
|
||||
RUN apk add --no-cache \
|
||||
apache2 \
|
||||
php81 \
|
||||
php81-common \
|
||||
php81-apache2 \
|
||||
php81-curl \
|
||||
php81-ldap \
|
||||
php81-mysqli \
|
||||
php81-gd \
|
||||
php81-xml \
|
||||
php81-mbstring \
|
||||
php81-zip \
|
||||
php81-ctype \
|
||||
php81-tokenizer \
|
||||
php81-pdo_mysql \
|
||||
php81-openssl \
|
||||
php81-bcmath \
|
||||
php81-phar \
|
||||
php81-json \
|
||||
php81-iconv \
|
||||
php81-fileinfo \
|
||||
php81-simplexml \
|
||||
php81-session \
|
||||
php81-dom \
|
||||
php81-xmlwriter \
|
||||
php81-xmlreader \
|
||||
php81-sodium \
|
||||
php81-redis \
|
||||
php81-pecl-memcached \
|
||||
php81-exif \
|
||||
php7 \
|
||||
php7-common \
|
||||
php7-apache2 \
|
||||
php7-curl \
|
||||
php7-ldap \
|
||||
php7-mysqli \
|
||||
php7-gd \
|
||||
php7-xml \
|
||||
php7-mbstring \
|
||||
php7-zip \
|
||||
php7-ctype \
|
||||
php7-tokenizer \
|
||||
php7-pdo_mysql \
|
||||
php7-openssl \
|
||||
php7-bcmath \
|
||||
php7-phar \
|
||||
php7-json \
|
||||
php7-iconv \
|
||||
php7-fileinfo \
|
||||
php7-simplexml \
|
||||
php7-session \
|
||||
php7-dom \
|
||||
php7-xmlwriter \
|
||||
php7-xmlreader \
|
||||
php7-sodium \
|
||||
curl \
|
||||
wget \
|
||||
vim \
|
||||
@@ -42,7 +39,7 @@ COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
|
||||
# Where apache's PID lives
|
||||
RUN mkdir -p /run/apache2 && chown apache:apache /run/apache2
|
||||
|
||||
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php81/php.ini
|
||||
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php7/php.ini
|
||||
COPY docker/000-default-2.4.conf /etc/apache2/conf.d/default.conf
|
||||
|
||||
# Enable mod_rewrite
|
||||
@@ -87,4 +84,4 @@ ENTRYPOINT ["/sbin/tini", "--"]
|
||||
|
||||
CMD ["/entrypoint.sh"]
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 80
|
||||
@@ -1,8 +1,8 @@
|
||||
ARG ENVIRONMENT=production
|
||||
ARG SNIPEIT_RELEASE=6.1.0
|
||||
ARG PHP_VERSION=8.2
|
||||
ARG PHP_ALPINE_VERSION=3.17
|
||||
ARG COMPOSER_VERSION=2
|
||||
ARG SNIPEIT_RELEASE=5.1.3
|
||||
ARG PHP_VERSION=7.4.16
|
||||
ARG PHP_ALPINE_VERSION=3.13
|
||||
ARG COMPOSER_VERSION=2.0.11
|
||||
|
||||
# Cannot use arguments with 'COPY --from' workaround
|
||||
# https://github.com/moby/moby/issues/34482#issuecomment-454716952
|
||||
@@ -52,7 +52,7 @@ RUN { \
|
||||
|
||||
# Install php extensions inside docker containers easily
|
||||
# https://github.com/mlocati/docker-php-extension-installer
|
||||
COPY --from=mlocati/php-extension-installer:2.1.15 /usr/bin/install-php-extensions /usr/local/bin/
|
||||
COPY --from=mlocati/php-extension-installer:1.2.19 /usr/bin/install-php-extensions /usr/local/bin/
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
bcmath \
|
||||
@@ -75,14 +75,14 @@ RUN set -eux; \
|
||||
rm snipeit.tar.gz; \
|
||||
# Install composer php dependencies
|
||||
if [ "$ENVIRONMENT" = "production" ]; then \
|
||||
echo "production environment detected!"; \
|
||||
echo "production enviroment detected!"; \
|
||||
composer update \
|
||||
--no-cache \
|
||||
--no-dev \
|
||||
--optimize-autoloader \
|
||||
--working-dir=/var/www/html; \
|
||||
else \
|
||||
echo "development environment detected!"; \
|
||||
echo "development enviroment detected!"; \
|
||||
apk add --no-cache \
|
||||
${DEV_PACKAGES}; \
|
||||
composer update \
|
||||
@@ -100,4 +100,4 @@ COPY --chown=www-data:www-data docker/docker-secrets.env /var/www/html/.env
|
||||
COPY --chmod=655 docker/docker-entrypoint.sh /usr/local/bin/docker-snipeit-entrypoint
|
||||
COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
|
||||
ENTRYPOINT [ "/usr/local/bin/docker-snipeit-entrypoint" ]
|
||||
CMD [ "/usr/local/bin/docker-php-entrypoint", "php-fpm" ]
|
||||
CMD [ "/usr/local/bin/docker-php-entrypoint", "php-fpm" ]
|
||||
93
README.md
93
README.md
@@ -1,18 +1,15 @@
|
||||

|
||||
|
||||
[](https://crowdin.com/project/snipe-it) [](https://hub.docker.com/r/snipe/snipe-it/) [](https://twitter.com/snipeitapp) [](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade) [](https://github.com/snipe/snipe-it/actions/workflows/tests.yml)
|
||||
[](#contributing) [](https://discord.gg/yZFtShAcKk)
|
||||
 [](https://crowdin.com/project/snipe-it) [](https://hub.docker.com/r/snipe/snipe-it/) [](https://twitter.com/snipeitapp) [](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade)
|
||||
[](#contributors) [](https://discord.gg/yZFtShAcKk) [](https://huntr.dev)
|
||||
|
||||
## Snipe-IT - Open Source Asset Management System
|
||||
|
||||
This is a FOSS project for asset management in IT Operations. Knowing who has which laptop, when it was purchased in order to depreciate it correctly, handling software licenses, etc.
|
||||
|
||||
It is built on [Laravel 8](http://laravel.com).
|
||||
It is built on [Laravel 6](http://laravel.com).
|
||||
|
||||
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]
|
||||
> __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, 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, 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.
|
||||
|
||||
-----
|
||||
|
||||
@@ -22,7 +19,7 @@ For instructions on installing and configuring Snipe-IT on your server, check ou
|
||||
|
||||
If you're having trouble with the installation, please check the [Common Issues](https://snipe-it.readme.io/docs/common-issues) and [Getting Help](https://snipe-it.readme.io/docs/getting-help) documentation, and search this repository's open *and* closed issues for help.
|
||||
|
||||
<!-- [](https://heroku.com/deploy) -->
|
||||
[](https://heroku.com/deploy)
|
||||
|
||||
-----
|
||||
### User's Manual
|
||||
@@ -33,9 +30,8 @@ For help using Snipe-IT, check out the [user's manual](https://snipe-it.readme.i
|
||||
|
||||
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]
|
||||
> **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.**
|
||||
|
||||
-----
|
||||
|
||||
### Upgrading
|
||||
@@ -59,25 +55,18 @@ Please see the [translations documentation](https://snipe-it.readme.io/docs/tran
|
||||
|
||||
Since the release of the JSON REST API, several third-party developers have been developing modules and libraries to work with Snipe-IT.
|
||||
|
||||
> [!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. :)
|
||||
|
||||
- [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)
|
||||
- [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
|
||||
- [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
|
||||
- [jamf2snipe](https://github.com/ParadoxGuitarist/jamf2snipe) by [@ParadoxGuitarist](https://github.com/ParadoxGuitarist) - Python script to sync assets between a JAMFPro instance and a Snipe-IT instance
|
||||
- [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)
|
||||
- [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 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
|
||||
- [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://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.
|
||||
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by @ReticentRobot - Windows agent for Snipe-IT
|
||||
|
||||
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. :)
|
||||
|
||||
-----
|
||||
|
||||
@@ -85,15 +74,65 @@ Since the release of the JSON REST API, several third-party developers have been
|
||||
|
||||
Please see the documentation on [contributing and developing for Snipe-IT](https://snipe-it.readme.io/docs/contributing-overview).
|
||||
|
||||
|
||||
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
|
||||
|
||||
The ERD is available [online here](https://drawsql.app/templates/snipe-it).
|
||||
|
||||
[Here is a list](CONTRIBUTORS.md) of the wonderful people that have contributed to the Snipe-IT.
|
||||
|
||||
-----
|
||||
|
||||
### Security
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **To report a security vulnerability, please email security@snipeitapp.com instead of using the issue tracker.**
|
||||
To report a security vulnerability, please email security@snipeitapp.com instead of using the issue tracker.
|
||||
|
||||
-----
|
||||
|
||||
### Contributors
|
||||
|
||||
Thanks goes to all of these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) who have helped Snipe-IT get this far:
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
| [<img src="https://avatars3.githubusercontent.com/u/197404?v=3" width="110px;"/><br /><sub>snipe</sub>](http://www.snipe.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=snipe "Code") [🚇](#infra-snipe "Infrastructure (Hosting, Build-Tools, etc)") [📖](https://github.com/snipe/snipe-it/commits?author=snipe "Documentation") [⚠️](https://github.com/snipe/snipe-it/commits?author=snipe "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asnipe "Bug reports") [🎨](#design-snipe "Design") [👀](#review-snipe "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/36335?v=3" width="110px;"/><br /><sub>Brady Wetherington</sub>](http://www.uberbrady.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=uberbrady "Code") [📖](https://github.com/snipe/snipe-it/commits?author=uberbrady "Documentation") [🚇](#infra-uberbrady "Infrastructure (Hosting, Build-Tools, etc)") [👀](#review-uberbrady "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/3803132?v=3" width="110px;"/><br /><sub>Daniel Meltzer</sub>](https://github.com/dmeltzer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Tests") [📖](https://github.com/snipe/snipe-it/commits?author=dmeltzer "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1609106?v=3" width="110px;"/><br /><sub>Michael T</sub>](http://www.tuckertechonline.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mtucker6784 "Code") | [<img src="https://avatars2.githubusercontent.com/u/3274937?v=3" width="110px;"/><br /><sub>madd15</sub>](https://github.com/madd15)<br />[📖](https://github.com/snipe/snipe-it/commits?author=madd15 "Documentation") [💬](#question-madd15 "Answering Questions") | [<img src="https://avatars2.githubusercontent.com/u/894126?v=3" width="110px;"/><br /><sub>Vincent Sposato</sub>](https://github.com/vsposato)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vsposato "Code") | [<img src="https://avatars0.githubusercontent.com/u/1639757?v=3" width="110px;"/><br /><sub>Andrea Bergamasco</sub>](https://github.com/vjandrea)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vjandrea "Code") |
|
||||
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||
| [<img src="https://avatars0.githubusercontent.com/u/10640152?v=3" width="110px;"/><br /><sub>Karol</sub>](https://github.com/kpawelski)<br />[🌍](#translation-kpawelski "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=kpawelski "Code") | [<img src="https://avatars3.githubusercontent.com/u/600106?v=3" width="110px;"/><br /><sub>morph027</sub>](http://blog.morph027.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=morph027 "Code") | [<img src="https://avatars3.githubusercontent.com/u/22935755?v=3" width="110px;"/><br /><sub>fvleminckx</sub>](https://github.com/fvleminckx)<br />[🚇](#infra-fvleminckx "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars2.githubusercontent.com/u/15633547?v=3" width="110px;"/><br /><sub>itsupportcmsukorg</sub>](https://github.com/itsupportcmsukorg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=itsupportcmsukorg "Code") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aitsupportcmsukorg "Bug reports") | [<img src="https://avatars3.githubusercontent.com/u/12373799?v=3" width="110px;"/><br /><sub>Frank</sub>](https://override.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=base-zero "Code") | [<img src="https://avatars0.githubusercontent.com/u/10137?v=3" width="110px;"/><br /><sub>Deleted user</sub>](https://github.com/ghost)<br />[🌍](#translation-ghost "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=ghost "Code") | [<img src="https://avatars1.githubusercontent.com/u/10802313?v=3" width="110px;"/><br /><sub>tiagom62</sub>](https://github.com/tiagom62)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tiagom62 "Code") [🚇](#infra-tiagom62 "Infrastructure (Hosting, Build-Tools, etc)") |
|
||||
| [<img src="https://avatars3.githubusercontent.com/u/2389047?v=3" width="110px;"/><br /><sub>Ryan Stafford</sub>](https://github.com/rystaf)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rystaf "Code") | [<img src="https://avatars2.githubusercontent.com/u/10345935?v=3" width="110px;"/><br /><sub>Eammon Hanlon</sub>](https://github.com/ehanlon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ehanlon "Code") | [<img src="https://avatars0.githubusercontent.com/u/441924?v=3" width="110px;"/><br /><sub>zjean</sub>](https://github.com/zjean)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zjean "Code") | [<img src="https://avatars0.githubusercontent.com/u/12660103?v=3" width="110px;"/><br /><sub>Matthias Frei</sub>](http://www.frei.media)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FREImedia "Code") | [<img src="https://avatars0.githubusercontent.com/u/3767518?v=3" width="110px;"/><br /><sub>opsydev</sub>](https://github.com/opsydev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=opsydev "Code") | [<img src="https://avatars1.githubusercontent.com/u/82290?v=3" width="110px;"/><br /><sub>Daniel Dreier</sub>](http://www.ddreier.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ddreier "Code") | [<img src="https://avatars0.githubusercontent.com/u/23448?v=3" width="110px;"/><br /><sub>Nikolai Prokoschenko</sub>](http://rassie.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rassie "Code") |
|
||||
| [<img src="https://avatars0.githubusercontent.com/u/13452757?v=3" width="110px;"/><br /><sub>Drew</sub>](https://github.com/YetAnotherCodeMonkey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=YetAnotherCodeMonkey "Code") | [<img src="https://avatars0.githubusercontent.com/u/1342320?v=3" width="110px;"/><br /><sub>Walter</sub>](https://github.com/merid14)<br />[💻](https://github.com/snipe/snipe-it/commits?author=merid14 "Code") | [<img src="https://avatars3.githubusercontent.com/u/11254614?v=3" width="110px;"/><br /><sub>Petr Baloun</sub>](https://github.com/balous)<br />[💻](https://github.com/snipe/snipe-it/commits?author=balous "Code") | [<img src="https://avatars0.githubusercontent.com/u/6117660?v=3" width="110px;"/><br /><sub>reidblomquist</sub>](https://github.com/reidblomquist)<br />[📖](https://github.com/snipe/snipe-it/commits?author=reidblomquist "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/539914?v=3" width="110px;"/><br /><sub>Mathieu Kooiman</sub>](https://github.com/mathieuk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mathieuk "Code") | [<img src="https://avatars3.githubusercontent.com/u/6606421?v=3" width="110px;"/><br /><sub>csayre</sub>](https://github.com/csayre)<br />[📖](https://github.com/snipe/snipe-it/commits?author=csayre "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/768488?v=3" width="110px;"/><br /><sub>Adam Dunson</sub>](https://github.com/adamdunson)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamdunson "Code") |
|
||||
| [<img src="https://avatars0.githubusercontent.com/u/5547470?v=3" width="110px;"/><br /><sub>Hereward</sub>](https://github.com/thehereward)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thehereward "Code") | [<img src="https://avatars0.githubusercontent.com/u/5802977?v=3" width="110px;"/><br /><sub>swoopdk</sub>](https://github.com/swoopdk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=swoopdk "Code") | [<img src="https://avatars1.githubusercontent.com/u/3470403?v=3" width="110px;"/><br /><sub>Abdullah Alansari</sub>](https://linkedin.com/in/ahimta)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Ahimta "Code") | [<img src="https://avatars0.githubusercontent.com/u/796443?v=3" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MicaelRodrigues "Code") | [<img src="https://avatars0.githubusercontent.com/u/614564?v=3" width="110px;"/><br /><sub>Patrick Gallagher</sub>](http://macadmincorner.com)<br />[📖](https://github.com/snipe/snipe-it/commits?author=patgmac "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/7165922?v=3" width="110px;"/><br /><sub>Miliamber</sub>](https://github.com/Miliamber)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Miliamber "Code") | [<img src="https://avatars3.githubusercontent.com/u/861766?v=3" width="110px;"/><br /><sub>hawk554</sub>](https://github.com/hawk554)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hawk554 "Code") |
|
||||
| [<img src="https://avatars1.githubusercontent.com/u/1695622?v=3" width="110px;"/><br /><sub>Justin Kerr</sub>](http://jbirdkerr.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jbirdkerr "Code") | [<img src="https://avatars3.githubusercontent.com/u/11426176?v=3" width="110px;"/><br /><sub>Ira W. Snyder</sub>](http://www.irasnyder.com/devel/)<br />[📖](https://github.com/snipe/snipe-it/commits?author=irasnyd "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/2475759?v=3" width="110px;"/><br /><sub>Aladin Alaily</sub>](https://github.com/aalaily)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aalaily "Code") | [<img src="https://avatars0.githubusercontent.com/u/10247644?v=3" width="110px;"/><br /><sub>Chase Hansen</sub>](https://github.com/kobie-chasehansen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kobie-chasehansen "Code") [💬](#question-kobie-chasehansen "Answering Questions") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3Akobie-chasehansen "Bug reports") | [<img src="https://avatars2.githubusercontent.com/u/13545400?v=3" width="110px;"/><br /><sub>IDM Helpdesk</sub>](https://github.com/IDM-Helpdesk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=IDM-Helpdesk "Code") | [<img src="https://avatars2.githubusercontent.com/u/614439?v=3" width="110px;"/><br /><sub>Kai</sub>](http://balticer.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=balticer "Code") | [<img src="https://avatars1.githubusercontent.com/u/8762511?v=3" width="110px;"/><br /><sub>Michael Daniels</sub>](http://www.michaeldaniels.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mdaniels5757 "Code") |
|
||||
| [<img src="https://avatars3.githubusercontent.com/u/1532660?v=3" width="110px;"/><br /><sub>Tom Castleman</sub>](http://tomcastleman.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tomcastleman "Code") | [<img src="https://avatars3.githubusercontent.com/u/10723243?v=3" width="110px;"/><br /><sub>Daniel Nemanic</sub>](https://github.com/DanielNemanic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DanielNemanic "Code") | [<img src="https://avatars0.githubusercontent.com/u/150648?v=3" width="110px;"/><br /><sub>SouthWolf</sub>](https://github.com/southwolf)<br />[💻](https://github.com/snipe/snipe-it/commits?author=southwolf "Code") | [<img src="https://avatars2.githubusercontent.com/u/131616?v=3" width="110px;"/><br /><sub>Ivar Nesje</sub>](https://github.com/ivarne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ivarne "Code") | [<img src="https://avatars1.githubusercontent.com/u/62333?v=3" width="110px;"/><br /><sub>Jérémy Benoist</sub>](http://www.j0k3r.net)<br />[📖](https://github.com/snipe/snipe-it/commits?author=j0k3r "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/724344?v=3" width="110px;"/><br /><sub>Chris Leathley</sub>](https://github.com/cleathley)<br />[🚇](#infra-cleathley "Infrastructure (Hosting, Build-Tools, etc)") | [<img src="https://avatars0.githubusercontent.com/u/972498?v=3" width="110px;"/><br /><sub>splaer</sub>](https://github.com/splaer)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Asplaer "Bug reports") [💻](https://github.com/snipe/snipe-it/commits?author=splaer "Code") |
|
||||
| [<img src="https://avatars1.githubusercontent.com/u/967362?v=3" width="110px;"/><br /><sub>Joe Ferguson</sub>](http://www.joeferguson.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=svpernova09 "Code") | [<img src="https://avatars3.githubusercontent.com/u/6108682?v=3" width="110px;"/><br /><sub>diwanicki</sub>](https://github.com/diwanicki)<br />[💻](https://github.com/snipe/snipe-it/commits?author=diwanicki "Code") [📖](https://github.com/snipe/snipe-it/commits?author=diwanicki "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/2527115?v=3" width="110px;"/><br /><sub>Lee Thoong Ching</sub>](https://github.com/pakkua80)<br />[📖](https://github.com/snipe/snipe-it/commits?author=pakkua80 "Documentation") [💻](https://github.com/snipe/snipe-it/commits?author=pakkua80 "Code") | [<img src="https://avatars1.githubusercontent.com/u/461491?v=3" width="110px;"/><br /><sub>Marek Šuppa</sub>](http://shu.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrshu "Code") | [<img src="https://avatars1.githubusercontent.com/u/8693762?v=3" width="110px;"/><br /><sub>Juan J. Martinez</sub>](https://github.com/mizar1616)<br />[🌍](#translation-mizar1616 "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1458388?v=3" width="110px;"/><br /><sub>R Ryan Dial</sub>](https://github.com/rrdial)<br />[🌍](#translation-rrdial "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2871745?v=3" width="110px;"/><br /><sub>Andrej Manduch</sub>](https://github.com/burlito)<br />[📖](https://github.com/snipe/snipe-it/commits?author=burlito "Documentation") |
|
||||
| [<img src="https://avatars0.githubusercontent.com/u/8341172?v=3" width="110px;"/><br /><sub>Jay Richards</sub>](http://www.cordeos.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=technogenus "Code") | [<img src="https://avatars2.githubusercontent.com/u/7295127?v=3" width="110px;"/><br /><sub>Alexander Innes</sub>](https://necurity.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leostat "Code") | [<img src="https://avatars2.githubusercontent.com/u/334485?v=3" width="110px;"/><br /><sub>Danny Garcia</sub>](https://buzzedword.codes)<br />[💻](https://github.com/snipe/snipe-it/commits?author=buzzedword "Code") | [<img src="https://avatars2.githubusercontent.com/u/366855?v=3" width="110px;"/><br /><sub>archpoint</sub>](https://github.com/archpoint)<br />[💻](https://github.com/snipe/snipe-it/commits?author=archpoint "Code") | [<img src="https://avatars1.githubusercontent.com/u/67991?v=3" width="110px;"/><br /><sub>Jake McGraw</sub>](http://www.jakemcgraw.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jakemcgraw "Code") | [<img src="https://avatars1.githubusercontent.com/u/1714374?v=3" width="110px;"/><br /><sub>FleischKarussel</sub>](https://github.com/FleischKarussel)<br />[📖](https://github.com/snipe/snipe-it/commits?author=FleischKarussel "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/319644?v=3" width="110px;"/><br /><sub>Dylan Yi</sub>](https://github.com/feeva)<br />[💻](https://github.com/snipe/snipe-it/commits?author=feeva "Code") |
|
||||
| [<img src="https://avatars2.githubusercontent.com/u/857740?v=3" width="110px;"/><br /><sub>Gil Rutkowski</sub>](http://FlashingCursor.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=flashingcursor "Code") | [<img src="https://avatars3.githubusercontent.com/u/129360?v=3" width="110px;"/><br /><sub>Desmond Morris</sub>](http://www.desmondmorris.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=desmondmorris "Code") | [<img src="https://avatars2.githubusercontent.com/u/52936?v=3" width="110px;"/><br /><sub>Nick Peelman</sub>](http://peelman.us)<br />[💻](https://github.com/snipe/snipe-it/commits?author=peelman "Code") | [<img src="https://avatars0.githubusercontent.com/u/53161?v=3" width="110px;"/><br /><sub>Abraham Vegh</sub>](https://abrahamvegh.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=abrahamvegh "Code") | [<img src="https://avatars0.githubusercontent.com/u/2818680?v=3" width="110px;"/><br /><sub>Mohamed Rashid</sub>](https://github.com/rashivkp)<br />[📖](https://github.com/snipe/snipe-it/commits?author=rashivkp "Documentation") | [<img src="https://avatars3.githubusercontent.com/u/1509456?v=3" width="110px;"/><br /><sub>Kasey</sub>](http://hinchk.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=HinchK "Code") | [<img src="https://avatars2.githubusercontent.com/u/10522541?v=3" width="110px;"/><br /><sub>Brett</sub>](https://github.com/BrettFagerlund)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=BrettFagerlund "Tests") |
|
||||
| [<img src="https://avatars2.githubusercontent.com/u/16108587?v=3" width="110px;"/><br /><sub>Jason Spriggs</sub>](http://jasonspriggs.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jasonspriggs "Code") | [<img src="https://avatars2.githubusercontent.com/u/1134568?v=3" width="110px;"/><br /><sub>Nate Felton</sub>](http://n8felton.wordpress.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=n8felton "Code") | [<img src="https://avatars2.githubusercontent.com/u/14036694?v=3" width="110px;"/><br /><sub>Manasses Ferreira</sub>](http://homepages.dcc.ufmg.br/~manassesferreira)<br />[💻](https://github.com/snipe/snipe-it/commits?author=manassesferreira "Code") | [<img src="https://avatars0.githubusercontent.com/u/15913949?v=3" width="110px;"/><br /><sub>Steve</sub>](https://github.com/steveelwood)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=steveelwood "Tests") | [<img src="https://avatars1.githubusercontent.com/u/3361683?v=3" width="110px;"/><br /><sub>matc</sub>](http://twitter.com/matc)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=matc "Tests") | [<img src="https://avatars3.githubusercontent.com/u/7405702?v=3" width="110px;"/><br /><sub>Cole R. Davis</sub>](http://www.davisracingteam.com)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=VanillaNinjaD "Tests") | [<img src="https://avatars2.githubusercontent.com/u/10167681?v=3" width="110px;"/><br /><sub>gibsonjoshua55</sub>](https://github.com/gibsonjoshua55)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gibsonjoshua55 "Code") |
|
||||
| [<img src="https://avatars2.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://github.com/zwerch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zwerch "Code") | [<img src="https://avatars0.githubusercontent.com/u/6961695?v=4" width="110px;"/><br /><sub>Iman</sub>](https://github.com/imanghafoori1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=imanghafoori1 "Code") | [<img src="https://avatars1.githubusercontent.com/u/6551003?v=4" width="110px;"/><br /><sub>Richard Hofman</sub>](https://github.com/richardhofman6)<br />[💻](https://github.com/snipe/snipe-it/commits?author=richardhofman6 "Code") | [<img src="https://avatars0.githubusercontent.com/u/3697569?v=4" width="110px;"/><br /><sub>gizzmojr</sub>](https://github.com/gizzmojr)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gizzmojr "Code") | [<img src="https://avatars3.githubusercontent.com/u/404729?v=4" width="110px;"/><br /><sub>Jenny Li</sub>](https://github.com/imjennyli)<br />[📖](https://github.com/snipe/snipe-it/commits?author=imjennyli "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/869227?v=4" width="110px;"/><br /><sub>Geoff Young</sub>](https://github.com/GeoffYoung)<br />[💻](https://github.com/snipe/snipe-it/commits?author=GeoffYoung "Code") | [<img src="https://avatars3.githubusercontent.com/u/1068477?v=4" width="110px;"/><br /><sub>Elliot Blackburn</sub>](http://www.elliotblackburn.com)<br />[📖](https://github.com/snipe/snipe-it/commits?author=BlueHatbRit "Documentation") |
|
||||
| [<img src="https://avatars1.githubusercontent.com/u/6357451?v=4" width="110px;"/><br /><sub>Tõnis Ormisson</sub>](http://andmemasin.eu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TonisOrmisson "Code") | [<img src="https://avatars0.githubusercontent.com/u/449411?v=4" width="110px;"/><br /><sub>Nicolai Essig</sub>](http://www.nicolai-essig.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thakilla "Code") | [<img src="https://avatars1.githubusercontent.com/u/14809698?v=4" width="110px;"/><br /><sub>Danielle</sub>](https://github.com/techincolor)<br />[📖](https://github.com/snipe/snipe-it/commits?author=techincolor "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/18545156?v=4" width="110px;"/><br /><sub>Lawrence</sub>](https://github.com/TheVakman)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=TheVakman "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3ATheVakman "Bug reports") | [<img src="https://avatars1.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>uknzaeinozpas</sub>](https://github.com/uknzaeinozpas)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Tests") [💻](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Code") | [<img src="https://avatars3.githubusercontent.com/u/422752?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/Gelob)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Gelob "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/10672546?v=4" width="110px;"/><br /><sub>vcordes79</sub>](https://github.com/vcordes79)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vcordes79 "Code") |
|
||||
| [<img src="https://avatars3.githubusercontent.com/u/27958330?v=4" width="110px;"/><br /><sub>fordster78</sub>](https://github.com/fordster78)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fordster78 "Code") | [<img src="https://avatars0.githubusercontent.com/u/34064225?v=4" width="110px;"/><br /><sub>CronKz</sub>](https://github.com/CronKz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CronKz "Code") [🌍](#translation-CronKz "Translation") | [<img src="https://avatars1.githubusercontent.com/u/585486?v=4" width="110px;"/><br /><sub>Tim Bishop</sub>](https://github.com/tdb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tdb "Code") | [<img src="https://avatars2.githubusercontent.com/u/5384694?v=4" width="110px;"/><br /><sub>Sean McIlvenna</sub>](https://www.seanmcilvenna.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanmcilvenna "Code") | [<img src="https://avatars3.githubusercontent.com/u/36515590?v=4" width="110px;"/><br /><sub>cepacs</sub>](https://github.com/cepacs)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Acepacs "Bug reports") [📖](https://github.com/snipe/snipe-it/commits?author=cepacs "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/37537300?v=4" width="110px;"/><br /><sub>lea-mink</sub>](https://github.com/lea-mink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lea-mink "Code") | [<img src="https://avatars0.githubusercontent.com/u/7140719?v=4" width="110px;"/><br /><sub>Hannah Tinkler</sub>](https://github.com/hannahtinkler)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hannahtinkler "Code") |
|
||||
| [<img src="https://avatars1.githubusercontent.com/u/1086388?v=4" width="110px;"/><br /><sub>Doeke Zanstra</sub>](https://github.com/doekman)<br />[💻](https://github.com/snipe/snipe-it/commits?author=doekman "Code") | [<img src="https://avatars1.githubusercontent.com/u/4325936?v=4" width="110px;"/><br /><sub>Djamon Staal</sub>](https://www.sdhd.nl/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=SjamonDaal "Code") | [<img src="https://avatars3.githubusercontent.com/u/12306859?v=4" width="110px;"/><br /><sub>Earl Ramirez</sub>](https://github.com/EarlRamirez)<br />[💻](https://github.com/snipe/snipe-it/commits?author=EarlRamirez "Code") | [<img src="https://avatars2.githubusercontent.com/u/8671456?v=4" width="110px;"/><br /><sub>Richard Ray Thomas</sub>](https://github.com/RichardRay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=RichardRay "Code") | [<img src="https://avatars3.githubusercontent.com/u/1852688?v=4" width="110px;"/><br /><sub>Ryan Kuba</sub>](https://www.taisun.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thelamer "Code") | [<img src="https://avatars1.githubusercontent.com/u/6751928?v=4" width="110px;"/><br /><sub>Brian Monroe</sub>](https://github.com/ParadoxGuitarist)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ParadoxGuitarist "Code") | [<img src="https://avatars1.githubusercontent.com/u/605167?v=4" width="110px;"/><br /><sub>plexorama</sub>](https://github.com/plexorama)<br />[💻](https://github.com/snipe/snipe-it/commits?author=plexorama "Code") |
|
||||
| [<img src="https://avatars2.githubusercontent.com/u/1795149?v=4" width="110px;"/><br /><sub>Till Deeke</sub>](https://tilldeeke.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tilldeeke "Code") | [<img src="https://avatars0.githubusercontent.com/u/12634129?v=4" width="110px;"/><br /><sub>5quirrel</sub>](https://github.com/5quirrel)<br />[💻](https://github.com/snipe/snipe-it/commits?author=5quirrel "Code") | [<img src="https://avatars1.githubusercontent.com/u/13071957?v=4" width="110px;"/><br /><sub>Jason</sub>](https://github.com/jasonlshelton)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jasonlshelton "Code") | [<img src="https://avatars3.githubusercontent.com/u/7128321?v=4" width="110px;"/><br /><sub>Antti</sub>](https://github.com/chemfy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chemfy "Code") | [<img src="https://avatars3.githubusercontent.com/u/10080364?v=4" width="110px;"/><br /><sub>DeusMaximus</sub>](https://github.com/DeusMaximus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DeusMaximus "Code") | [<img src="https://avatars2.githubusercontent.com/u/16384611?v=4" width="110px;"/><br /><sub>a-royal</sub>](https://github.com/A-ROYAL)<br />[🌍](#translation-A-ROYAL "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5358208?v=4" width="110px;"/><br /><sub>Alberto Aldrigo</sub>](https://github.com/albertoaldrigo)<br />[🌍](#translation-albertoaldrigo "Translation") |
|
||||
| [<img src="https://avatars0.githubusercontent.com/u/1412342?v=4" width="110px;"/><br /><sub>Alex Stanev</sub>](http://alex.stanev.org/blog)<br />[🌍](#translation-RealEnder "Translation") | [<img src="https://avatars0.githubusercontent.com/u/177295?v=4" width="110px;"/><br /><sub>Andreas Rehm</sub>](http://devel.itsolution2.de)<br />[🌍](#translation-sirrus "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5080535?v=4" width="110px;"/><br /><sub>Andreas Erhard</sub>](https://github.com/xelan)<br />[🌍](#translation-xelan "Translation") | [<img src="https://avatars2.githubusercontent.com/u/142350?v=4" width="110px;"/><br /><sub>Andrés Vanegas Jiménez</sub>](https://github.com/angeldeejay)<br />[🌍](#translation-angeldeejay "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3910403?v=4" width="110px;"/><br /><sub>Antonio Schiavon</sub>](https://github.com/aschiavon91)<br />[🌍](#translation-aschiavon91 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10464547?v=4" width="110px;"/><br /><sub>benunter</sub>](https://github.com/benunter)<br />[🌍](#translation-benunter "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5038647?v=4" width="110px;"/><br /><sub>Borys Żmuda</sub>](http://catweb24.pl)<br />[🌍](#translation-rudashi "Translation") |
|
||||
| [<img src="https://avatars0.githubusercontent.com/u/5539359?v=4" width="110px;"/><br /><sub>chibacityblues</sub>](https://github.com/chibacityblues)<br />[🌍](#translation-chibacityblues "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1954830?v=4" width="110px;"/><br /><sub>Chien Wei Lin</sub>](https://github.com/cwlin0416)<br />[🌍](#translation-cwlin0416 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/11700533?v=4" width="110px;"/><br /><sub>Christian Schuster</sub>](https://github.com/Againstreality)<br />[🌍](#translation-Againstreality "Translation") | [<img src="https://avatars1.githubusercontent.com/u/4308704?v=4" width="110px;"/><br /><sub>Christian Stefanus</sub>](http://chriss.webhostid.com)<br />[🌍](#translation-kopi-item "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3009327?v=4" width="110px;"/><br /><sub>wxcafé</sub>](http://wxcafe.net)<br />[🌍](#translation-wxcafe "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35761525?v=4" width="110px;"/><br /><sub>dpyroc</sub>](https://github.com/dpyroc)<br />[🌍](#translation-dpyroc "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2153639?v=4" width="110px;"/><br /><sub>Daniel Friedlmaier</sub>](http://www.friedlmaier.net)<br />[🌍](#translation-da-friedl "Translation") |
|
||||
| [<img src="https://avatars1.githubusercontent.com/u/2947640?v=4" width="110px;"/><br /><sub>Daniel Heene</sub>](https://github.com/danielheene)<br />[🌍](#translation-danielheene "Translation") | [<img src="https://avatars3.githubusercontent.com/u/319022?v=4" width="110px;"/><br /><sub>danielcb</sub>](https://github.com/danielcb)<br />[🌍](#translation-danielcb "Translation") | [<img src="https://avatars3.githubusercontent.com/u/15846537?v=4" width="110px;"/><br /><sub>Dominik Senti</sub>](https://github.com/dominiksenti)<br />[🌍](#translation-dominiksenti "Translation") | [<img src="https://avatars0.githubusercontent.com/u/25570954?v=4" width="110px;"/><br /><sub>Eric Gautheron</sub>](http://www.konectik.com)<br />[🌍](#translation-EpixFr "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5732623?v=4" width="110px;"/><br /><sub>Erlend Pilø</sub>](https://erlpil.com)<br />[🌍](#translation-Erlpil "Translation") | [<img src="https://avatars0.githubusercontent.com/u/541832?v=4" width="110px;"/><br /><sub>Fabio Rapposelli</sub>](http://fabio.technology)<br />[🌍](#translation-frapposelli "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3605240?v=4" width="110px;"/><br /><sub>Felipe Barros</sub>](https://github.com/fgbs)<br />[🌍](#translation-fgbs "Translation") |
|
||||
| [<img src="https://avatars0.githubusercontent.com/u/257745?v=4" width="110px;"/><br /><sub>Fernando Possebon</sub>](https://github.com/possebon)<br />[🌍](#translation-possebon "Translation") | [<img src="https://avatars3.githubusercontent.com/u/2540832?v=4" width="110px;"/><br /><sub>gdraque</sub>](https://github.com/gdraque)<br />[🌍](#translation-gdraque "Translation") | [<img src="https://avatars0.githubusercontent.com/u/23440381?v=4" width="110px;"/><br /><sub>Georg Wallisch</sub>](https://github.com/georgwallisch)<br />[🌍](#translation-georgwallisch "Translation") | [<img src="https://avatars1.githubusercontent.com/u/9852832?v=4" width="110px;"/><br /><sub>Gerardo Robles</sub>](https://github.com/jgroblesr85)<br />[🌍](#translation-jgroblesr85 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/11082640?v=4" width="110px;"/><br /><sub>Gluek</sub>](https://t.me/Gluek)<br />[🌍](#translation-mrgluek "Translation") | [<img src="https://avatars0.githubusercontent.com/u/6847946?v=4" width="110px;"/><br /><sub>AdnanAbuShahad</sub>](https://github.com/AdnanAbuShahad)<br />[🌍](#translation-AdnanAbuShahad "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3580608?v=4" width="110px;"/><br /><sub>Hafidzi My</sub>](https://hafidzi.my)<br />[🌍](#translation-hafidzi "Translation") |
|
||||
| [<img src="https://avatars2.githubusercontent.com/u/205521?v=4" width="110px;"/><br /><sub>Harim Park</sub>](https://github.com/fofwisdom)<br />[🌍](#translation-fofwisdom "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3333841?v=4" width="110px;"/><br /><sub>Henrik Kentsson</sub>](http://www.kentsson.se)<br />[🌍](#translation-Kentsson "Translation") | [<img src="https://avatars0.githubusercontent.com/u/36551034?v=4" width="110px;"/><br /><sub>Husnul Yaqien</sub>](https://github.com/husnulyaqien)<br />[🌍](#translation-husnulyaqien "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2372747?v=4" width="110px;"/><br /><sub>Ibrahim</sub>](http://abaalkhail.org)<br />[🌍](#translation-abaalkh "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1389334?v=4" width="110px;"/><br /><sub>igolman</sub>](https://github.com/igolman)<br />[🌍](#translation-igolman "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3257070?v=4" width="110px;"/><br /><sub>itangiang</sub>](https://github.com/itangiang)<br />[🌍](#translation-itangiang "Translation") | [<img src="https://avatars2.githubusercontent.com/u/14814254?v=4" width="110px;"/><br /><sub>jarby1211</sub>](https://github.com/jarby1211)<br />[🌍](#translation-jarby1211 "Translation") |
|
||||
| [<img src="https://avatars3.githubusercontent.com/u/6719357?v=4" width="110px;"/><br /><sub>Jhonn Willker</sub>](http://jwillker.com)<br />[🌍](#translation-JohnWillker "Translation") | [<img src="https://avatars2.githubusercontent.com/u/10983635?v=4" width="110px;"/><br /><sub>Jose</sub>](https://github.com/joxelito94)<br />[🌍](#translation-joxelito94 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5206122?v=4" width="110px;"/><br /><sub>laopangzi</sub>](https://github.com/laopangzi)<br />[🌍](#translation-laopangzi "Translation") | [<img src="https://avatars2.githubusercontent.com/u/79707?v=4" width="110px;"/><br /><sub>Lars Strojny</sub>](http://usrportage.de)<br />[🌍](#translation-lstrojny "Translation") | [<img src="https://avatars0.githubusercontent.com/u/389801?v=4" width="110px;"/><br /><sub>MarcosBL</sub>](http://twitter.com/marcosbl)<br />[🌍](#translation-MarcosBL "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35664606?v=4" width="110px;"/><br /><sub>marie joy cajes</sub>](https://github.com/mariejoyacajes)<br />[🌍](#translation-mariejoyacajes "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3052816?v=4" width="110px;"/><br /><sub>Mark S. Johansen</sub>](http://www.markjohansen.dk)<br />[🌍](#translation-msjohansen "Translation") |
|
||||
| [<img src="https://avatars2.githubusercontent.com/u/982885?v=4" width="110px;"/><br /><sub>Martin Stub</sub>](http://martinstub.dk)<br />[🌍](#translation-stubben "Translation") | [<img src="https://avatars2.githubusercontent.com/u/28959963?v=4" width="110px;"/><br /><sub>Meyer Flavio</sub>](https://github.com/meyerf99)<br />[🌍](#translation-meyerf99 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/796443?v=4" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[🌍](#translation-MicaelRodrigues "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10481331?v=4" width="110px;"/><br /><sub>Mikael Rasmussen</sub>](http://rubixy.com/)<br />[🌍](#translation-mikaelssen "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1544552?v=4" width="110px;"/><br /><sub>IxFail</sub>](https://github.com/IxFail)<br />[🌍](#translation-IxFail "Translation") | [<img src="https://avatars3.githubusercontent.com/u/18483118?v=4" width="110px;"/><br /><sub>Mohammed Fota</sub>](http://www.mohammedfota.com)<br />[🌍](#translation-MohammedFota "Translation") | [<img src="https://avatars0.githubusercontent.com/u/227080?v=4" width="110px;"/><br /><sub>Moayad Alserihi</sub>](https://github.com/omego)<br />[🌍](#translation-omego "Translation") |
|
||||
| [<img src="https://avatars0.githubusercontent.com/u/1680266?v=4" width="110px;"/><br /><sub>saymd</sub>](https://github.com/saymd)<br />[🌍](#translation-saymd "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1826808?v=4" width="110px;"/><br /><sub>Patrik Larsson</sub>](https://nordsken.se)<br />[🌍](#translation-pooot "Translation") | [<img src="https://avatars1.githubusercontent.com/u/20584746?v=4" width="110px;"/><br /><sub>drcryo</sub>](https://github.com/drcryo)<br />[🌍](#translation-drcryo "Translation") | [<img src="https://avatars1.githubusercontent.com/u/19408004?v=4" width="110px;"/><br /><sub>pawel1615</sub>](https://github.com/pawel1615)<br />[🌍](#translation-pawel1615 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/23340468?v=4" width="110px;"/><br /><sub>bodrovics</sub>](https://github.com/bodrovics)<br />[🌍](#translation-bodrovics "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3257654?v=4" width="110px;"/><br /><sub>priatna</sub>](https://github.com/priatna)<br />[🌍](#translation-priatna "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5358374?v=4" width="110px;"/><br /><sub>Fan Jiang</sub>](https://amayume.net)<br />[🌍](#translation-ProfFan "Translation") |
|
||||
| [<img src="https://avatars1.githubusercontent.com/u/22555451?v=4" width="110px;"/><br /><sub>ragnarcx</sub>](https://github.com/ragnarcx)<br />[🌍](#translation-ragnarcx "Translation") | [<img src="https://avatars2.githubusercontent.com/u/18654582?v=4" width="110px;"/><br /><sub>Rein van Haaren</sub>](http://www.reinvanhaaren.nl/)<br />[🌍](#translation-reinvanhaaren "Translation") | [<img src="https://avatars1.githubusercontent.com/u/386672?v=4" width="110px;"/><br /><sub>Teguh Dwicaksana</sub>](http://dheche.songolimo.net)<br />[🌍](#translation-dheche "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2572552?v=4" width="110px;"/><br /><sub>fraccie</sub>](https://github.com/FRaccie)<br />[🌍](#translation-FRaccie "Translation") | [<img src="https://avatars0.githubusercontent.com/u/35182720?v=4" width="110px;"/><br /><sub>vinzruzell</sub>](https://github.com/vinzruzell)<br />[🌍](#translation-vinzruzell "Translation") | [<img src="https://avatars1.githubusercontent.com/u/7883603?v=4" width="110px;"/><br /><sub>Kevin Austin</sub>](http://kevinaustin.com)<br />[🌍](#translation-vipsystem "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3861828?v=4" width="110px;"/><br /><sub>Wira Sandy</sub>](http://azuraweb.xyz)<br />[🌍](#translation-wira-sandy "Translation") |
|
||||
| [<img src="https://avatars2.githubusercontent.com/u/8663789?v=4" width="110px;"/><br /><sub>Илья</sub>](https://github.com/GrayHoax)<br />[🌍](#translation-GrayHoax "Translation") | [<img src="https://avatars3.githubusercontent.com/u/30119111?v=4" width="110px;"/><br /><sub>GodUseVPN</sub>](https://github.com/godusevpn)<br />[🌍](#translation-godusevpn "Translation") | [<img src="https://avatars1.githubusercontent.com/u/745576?v=4" width="110px;"/><br /><sub>周周</sub>](https://github.com/EngrZhou)<br />[🌍](#translation-EngrZhou "Translation") | [<img src="https://avatars3.githubusercontent.com/u/1631095?v=4" width="110px;"/><br /><sub>Sam</sub>](https://github.com/takuy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=takuy "Code") | [<img src="https://avatars1.githubusercontent.com/u/264022?v=4" width="110px;"/><br /><sub>Azerothian</sub>](https://www.illisian.com.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azerothian "Code") | [<img src="https://avatars1.githubusercontent.com/u/4930051?v=4" width="110px;"/><br /><sub>Wes Hulette</sub>](http://macfoo.wordpress.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jwhulette "Code") | [<img src="https://avatars0.githubusercontent.com/u/8134591?v=4" width="110px;"/><br /><sub>patrict</sub>](https://github.com/patrict)<br />[💻](https://github.com/snipe/snipe-it/commits?author=patrict "Code") |
|
||||
| [<img src="https://avatars3.githubusercontent.com/u/2611616?v=4" width="110px;"/><br /><sub>Dmitriy Minaev</sub>](https://github.com/VELIKII-DIVAN)<br />[💻](https://github.com/snipe/snipe-it/commits?author=VELIKII-DIVAN "Code") | [<img src="https://avatars0.githubusercontent.com/u/5132245?v=4" width="110px;"/><br /><sub>liquidhorse</sub>](https://github.com/liquidhorse)<br />[💻](https://github.com/snipe/snipe-it/commits?author=liquidhorse "Code") | [<img src="https://avatars1.githubusercontent.com/u/183678?v=4" width="110px;"/><br /><sub>Jordi Boggiano</sub>](https://seld.be/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Seldaek "Code") | [<img src="https://avatars0.githubusercontent.com/u/653557?v=4" width="110px;"/><br /><sub>Ivan Nieto</sub>](https://github.com/inietov)<br />[💻](https://github.com/snipe/snipe-it/commits?author=inietov "Code") | [<img src="https://avatars2.githubusercontent.com/u/6764151?v=4" width="110px;"/><br /><sub>Ben RUBSON</sub>](https://github.com/benrubson)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benrubson "Code") | [<img src="https://avatars2.githubusercontent.com/u/8554558?v=4" width="110px;"/><br /><sub>NMathar</sub>](https://github.com/NMathar)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NMathar "Code") | [<img src="https://avatars1.githubusercontent.com/u/139566?v=4" width="110px;"/><br /><sub>Steffen</sub>](https://github.com/smb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smb "Code") |
|
||||
| [<img src="https://avatars0.githubusercontent.com/u/6609453?v=4" width="110px;"/><br /><sub>Sxderp</sub>](https://github.com/Sxderp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Sxderp "Code") | [<img src="https://avatars1.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>fanta8897</sub>](https://github.com/fanta8897)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fanta8897 "Code") | [<img src="https://avatars2.githubusercontent.com/u/2576509?v=4" width="110px;"/><br /><sub>Andrey Bolonin</sub>](https://andreybolonin.com/phpconsulting/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andreybolonin "Code") | [<img src="https://avatars3.githubusercontent.com/u/2173307?v=4" width="110px;"/><br /><sub>shinayoshi</sub>](http://www.shinayoshi.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=shinayoshi "Code") | [<img src="https://avatars3.githubusercontent.com/u/2130159?v=4" width="110px;"/><br /><sub>Hubert</sub>](https://github.com/reuser)<br />[💻](https://github.com/snipe/snipe-it/commits?author=reuser "Code") | [<img src="https://avatars0.githubusercontent.com/u/6865789?v=4" width="110px;"/><br /><sub>KeenRivals</sub>](https://brashear.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KeenRivals "Code") | [<img src="https://avatars3.githubusercontent.com/u/2902513?v=4" width="110px;"/><br /><sub>omyno</sub>](https://github.com/omyno)<br />[💻](https://github.com/snipe/snipe-it/commits?author=omyno "Code") |
|
||||
| [<img src="https://avatars1.githubusercontent.com/u/6271335?v=4" width="110px;"/><br /><sub>Evgeny</sub>](https://github.com/jackka)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jackka "Code") | [<img src="https://avatars2.githubusercontent.com/u/1169963?v=4" width="110px;"/><br /><sub>Colin Campbell</sub>](https://digitalist.se)<br />[💻](https://github.com/snipe/snipe-it/commits?author=colin-campbell "Code") | [<img src="https://avatars3.githubusercontent.com/u/2872098?v=4" width="110px;"/><br /><sub>Ľubomír Kučera</sub>](https://github.com/lubo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lubo "Code") | [<img src="https://avatars3.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://www.sourceguru.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Mezzle "Code") | [<img src="https://avatars1.githubusercontent.com/u/7632599?v=4" width="110px;"/><br /><sub>Tim Farmer</sub>](https://github.com/timothyfarmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=timothyfarmer "Code") | [<img src="https://avatars0.githubusercontent.com/u/17459600?v=4" width="110px;"/><br /><sub>Marián Skrip</sub>](https://github.com/mskrip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mskrip "Code") | [<img src="https://avatars2.githubusercontent.com/u/47435081?v=4" width="110px;"/><br /><sub>Godfrey Martinez</sub>](https://github.com/Godmartinz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Godmartinz "Code") |
|
||||
| [<img src="https://avatars1.githubusercontent.com/u/2075128?v=4" width="110px;"/><br /><sub>bigtreeEdo</sub>](https://github.com/bigtreeEdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bigtreeEdo "Code") | [<img src="https://avatars0.githubusercontent.com/u/5000430?v=4" width="110px;"/><br /><sub>Colin McNeil</sub>](https://colinmcneil.me/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ColinMcNeil "Code") | [<img src="https://avatars0.githubusercontent.com/u/421625?v=4" width="110px;"/><br /><sub>JoKneeMo</sub>](https://github.com/JoKneeMo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JoKneeMo "Code") | [<img src="https://avatars0.githubusercontent.com/u/54849013?v=4" width="110px;"/><br /><sub>Joshi</sub>](http://www.redbridge.se)<br />[💻](https://github.com/snipe/snipe-it/commits?author=joshi-redbridge "Code") | [<img src="https://avatars2.githubusercontent.com/u/15731458?v=4" width="110px;"/><br /><sub>Anthony Burns</sub>](https://github.com/anthonypburns)<br />[💻](https://github.com/snipe/snipe-it/commits?author=anthonypburns "Code") | [<img src="https://avatars1.githubusercontent.com/u/63399474?v=4" width="110px;"/><br /><sub>johnson-yi</sub>](https://github.com/johnson-yi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=johnson-yi "Code") | [<img src="https://avatars1.githubusercontent.com/u/1862720?v=4" width="110px;"/><br /><sub>Sanjay Govind</sub>](https://tangentmc.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sanjay900 "Code") |
|
||||
| [<img src="https://avatars0.githubusercontent.com/u/1255375?v=4" width="110px;"/><br /><sub>Peter Upfold</sub>](https://peter.upfold.org.uk/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PeterUpfold "Code") | [<img src="https://avatars2.githubusercontent.com/u/961717?v=4" width="110px;"/><br /><sub>Jared Biel</sub>](https://github.com/jbiel)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jbiel "Code") | [<img src="https://avatars1.githubusercontent.com/u/1733625?v=4" width="110px;"/><br /><sub>Dampfklon</sub>](https://github.com/dampfklon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dampfklon "Code") | [<img src="https://avatars2.githubusercontent.com/u/52973156?v=4" width="110px;"/><br /><sub>Charles Hamilton</sub>](https://communityclosing.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chamilton-ccn "Code") | [<img src="https://avatars.githubusercontent.com/u/551789?v=4" width="110px;"/><br /><sub>Giuseppe Iannello</sub>](https://github.com/giannello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=giannello "Code") | [<img src="https://avatars.githubusercontent.com/u/3691490?v=4" width="110px;"/><br /><sub>Peter Dave Hello</sub>](https://www.peterdavehello.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PeterDaveHello "Code") | [<img src="https://avatars.githubusercontent.com/u/6106332?v=4" width="110px;"/><br /><sub>sigmoidal</sub>](https://github.com/sigmoidal)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sigmoidal "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/2082554?v=4" width="110px;"/><br /><sub>Vincent Lainé</sub>](https://github.com/phenixdotnet)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phenixdotnet "Code") | [<img src="https://avatars.githubusercontent.com/u/1943040?v=4" width="110px;"/><br /><sub>Lucas Pleß</sub>](http://www.lucas-pless.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derlucas "Code") | [<img src="https://avatars.githubusercontent.com/u/472804?v=4" width="110px;"/><br /><sub>Ian Littman</sub>](http://twitter.com/iansltx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=iansltx "Code") | [<img src="https://avatars.githubusercontent.com/u/3519029?v=4" width="110px;"/><br /><sub>João Paulo</sub>](https://github.com/PauloLuna)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PauloLuna "Code") | [<img src="https://avatars.githubusercontent.com/u/70443365?v=4" width="110px;"/><br /><sub>ThoBur</sub>](https://github.com/ThoBur)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ThoBur "Code") | [<img src="https://avatars.githubusercontent.com/u/1972329?v=4" width="110px;"/><br /><sub>Alexander Chibrikin</sub>](http://phpprofi.ru/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alek13 "Code") | [<img src="https://avatars.githubusercontent.com/u/438332?v=4" width="110px;"/><br /><sub>Anthony Winstanley</sub>](https://github.com/winstan)<br />[💻](https://github.com/snipe/snipe-it/commits?author=winstan "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/3075214?v=4" width="110px;"/><br /><sub>Folke</sub>](https://github.com/fashberg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fashberg "Code") | [<img src="https://avatars.githubusercontent.com/u/1351571?v=4" width="110px;"/><br /><sub>Bennett Blodinger</sub>](https://github.com/benwa)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benwa "Code") | [<img src="https://avatars.githubusercontent.com/u/2974631?v=4" width="110px;"/><br /><sub>NMC</sub>](https://nmc.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ncareau "Code") | [<img src="https://avatars.githubusercontent.com/u/52182449?v=4" width="110px;"/><br /><sub>andres-baller</sub>](https://github.com/andres-baller)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andres-baller "Code") | [<img src="https://avatars.githubusercontent.com/u/67109348?v=4" width="110px;"/><br /><sub>sean-borg</sub>](https://github.com/sean-borg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sean-borg "Code") | [<img src="https://avatars.githubusercontent.com/u/32170051?v=4" width="110px;"/><br /><sub>EDVLeer</sub>](https://github.com/EDVLeer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=EDVLeer "Code") | [<img src="https://avatars.githubusercontent.com/u/23075196?v=4" width="110px;"/><br /><sub>Kurokat</sub>](https://github.com/Kurokat)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Kurokat "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/915514?v=4" width="110px;"/><br /><sub>Kevin Köllmann</sub>](https://www.kevinkoellmann.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koelle25 "Code") | [<img src="https://avatars.githubusercontent.com/u/49025941?v=4" width="110px;"/><br /><sub>sw-mreyes</sub>](https://github.com/sw-mreyes)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sw-mreyes "Code") | [<img src="https://avatars.githubusercontent.com/u/70129?v=4" width="110px;"/><br /><sub>Joel Pittet</sub>](https://pittet.ca)<br />[💻](https://github.com/snipe/snipe-it/commits?author=joelpittet "Code") | [<img src="https://avatars.githubusercontent.com/u/792695?v=4" width="110px;"/><br /><sub>Eli Young</sub>](https://elyscape.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=elyscape "Code") | [<img src="https://avatars.githubusercontent.com/u/317015?v=4" width="110px;"/><br /><sub>Raell Dottin</sub>](https://github.com/raelldottin)<br />[💻](https://github.com/snipe/snipe-it/commits?author=raelldottin "Code") | [<img src="https://avatars.githubusercontent.com/u/1446856?v=4" width="110px;"/><br /><sub>Tom Misilo</sub>](https://github.com/misilot)<br />[💻](https://github.com/snipe/snipe-it/commits?author=misilot "Code") | [<img src="https://avatars.githubusercontent.com/u/4496300?v=4" width="110px;"/><br /><sub>David Davenne</sub>](http://david.davenne.be)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JuustoMestari "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/9255772?v=4" width="110px;"/><br /><sub>Mark Stenglein</sub>](https://markstenglein.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ocelotsloth "Code") | [<img src="https://avatars.githubusercontent.com/u/35658596?v=4" width="110px;"/><br /><sub>ajsy</sub>](https://github.com/ajsy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ajsy "Code") | [<img src="https://avatars.githubusercontent.com/u/3628035?v=4" width="110px;"/><br /><sub>Jan Kiesewetter</sub>](https://github.com/t3easy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=t3easy "Code") | [<img src="https://avatars.githubusercontent.com/u/79449630?v=4" width="110px;"/><br /><sub>Tetrachloromethane250</sub>](https://github.com/Tetrachloromethane250)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tetrachloromethane250 "Code") | [<img src="https://avatars.githubusercontent.com/u/22004482?v=4" width="110px;"/><br /><sub>Lars Kajes</sub>](https://www.kajes.se/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kajes "Code") | [<img src="https://avatars.githubusercontent.com/u/13993216?v=4" width="110px;"/><br /><sub>Joly0</sub>](https://github.com/Joly0)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Joly0 "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>theburger</sub>](https://github.com/limeless)<br />[💻](https://github.com/snipe/snipe-it/commits?author=limeless "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/36065681?v=4" width="110px;"/><br /><sub>David Valin Alonso</sub>](https://github.com/deivishome)<br />[💻](https://github.com/snipe/snipe-it/commits?author=deivishome "Code") | [<img src="https://avatars.githubusercontent.com/u/8290389?v=4" width="110px;"/><br /><sub>andreaci</sub>](https://github.com/andreaci)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andreaci "Code") | [<img src="https://avatars.githubusercontent.com/u/1828542?v=4" width="110px;"/><br /><sub>Jelle Sebreghts</sub>](http://www.jellesebreghts.be)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Jelle-S "Code") | [<img src="https://avatars.githubusercontent.com/u/11180862?v=4" width="110px;"/><br /><sub>Michael Pietsch</sub>](https://github.com/Skywalker-11)<br /> | [<img src="https://avatars.githubusercontent.com/u/22068886?v=4" width="110px;"/><br /><sub>Masudul Haque Shihab</sub>](https://github.com/sh1hab)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sh1hab "Code") | [<img src="https://avatars.githubusercontent.com/u/16099942?v=4" width="110px;"/><br /><sub>Supapong Areeprasertkul</sub>](http://www.freedomdive.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zybersup "Code") | [<img src="https://avatars.githubusercontent.com/u/207358?v=4" width="110px;"/><br /><sub>Peter Sarossy</sub>](https://github.com/psarossy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=psarossy "Code") |
|
||||
| [<img src="https://avatars.githubusercontent.com/u/11823649?v=4" width="110px;"/><br /><sub>Renee Margaret McConahy</sub>](https://github.com/nepella)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nepella "Code") | [<img src="https://avatars.githubusercontent.com/u/5553884?v=4" width="110px;"/><br /><sub>JohnnyPicnic</sub>](https://github.com/JohnnyPicnic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JohnnyPicnic "Code") | [<img src="https://avatars.githubusercontent.com/u/8799594?v=4" width="110px;"/><br /><sub>markbrule</sub>](https://github.com/markbrule)<br />[💻](https://github.com/snipe/snipe-it/commits?author=markbrule "Code") | [<img src="https://avatars.githubusercontent.com/u/1962801?v=4" width="110px;"/><br /><sub>Mike Campbell</sub>](https://github.com/mikecmpbll)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikecmpbll "Code") | [<img src="https://avatars.githubusercontent.com/u/11973217?v=4" width="110px;"/><br /><sub>tbrconnect</sub>](https://github.com/tbrconnect)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tbrconnect "Code") | [<img src="https://avatars.githubusercontent.com/u/12447225?v=4" width="110px;"/><br /><sub>kcoyo</sub>](https://github.com/kcoyo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kcoyo "Code") | [<img src="https://avatars.githubusercontent.com/u/494017?v=4" width="110px;"/><br /><sub>Travis Miller</sub>](https://travismiller.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=travismiller "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/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") |
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
|
||||
65
TESTING.md
65
TESTING.md
@@ -1,65 +0,0 @@
|
||||
# Running the Test Suite
|
||||
|
||||
This document is targeted at developers looking to make modifications to this application's code base and want to run the existing test suite.
|
||||
|
||||
Before starting, follow the [instructions](README.md#installation) for installing the application locally and ensure you can load it in a browser properly.
|
||||
|
||||
## Unit and Feature Tests
|
||||
|
||||
Before attempting to run the test suite copy the example environment file for tests and update the values to match your environment:
|
||||
|
||||
`cp .env.testing.example .env.testing`
|
||||
|
||||
The following should work for running tests in memory with sqlite:
|
||||
```
|
||||
# --------------------------------------------
|
||||
# REQUIRED: BASIC APP SETTINGS
|
||||
# --------------------------------------------
|
||||
APP_ENV=testing
|
||||
APP_DEBUG=true
|
||||
APP_KEY=base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU=
|
||||
APP_URL=http://localhost:8000
|
||||
APP_TIMEZONE='UTC'
|
||||
APP_LOCALE=en
|
||||
|
||||
# --------------------------------------------
|
||||
# REQUIRED: DATABASE SETTINGS
|
||||
# --------------------------------------------
|
||||
DB_CONNECTION=sqlite_testing
|
||||
#DB_HOST=127.0.0.1
|
||||
#DB_PORT=3306
|
||||
#DB_DATABASE=null
|
||||
#DB_USERNAME=null
|
||||
#DB_PASSWORD=null
|
||||
```
|
||||
|
||||
To use MySQL you should update the `DB_` variables to match your local test database:
|
||||
```
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE={}
|
||||
DB_USERNAME={}
|
||||
DB_PASSWORD={}
|
||||
```
|
||||
|
||||
Now you are ready to run the entire test suite from your terminal:
|
||||
|
||||
```shell
|
||||
php artisan test
|
||||
````
|
||||
|
||||
To run individual test files, you can pass the path to the test that you want to run:
|
||||
|
||||
```shell
|
||||
php artisan test tests/Unit/AccessoryTest.php
|
||||
```
|
||||
|
||||
Some tests, like ones concerning LDAP, are marked with the `@group` annotation. Those groups can be run, or excluded, using the `--group` or `--exclude-group` flags:
|
||||
|
||||
```shell
|
||||
php artisan test --group=ldap
|
||||
|
||||
php artisan test --exclude-group=ldap
|
||||
```
|
||||
This can be helpful if a set of tests are failing because you don't have an extension, like LDAP, installed.
|
||||
6
app.json
6
app.json
@@ -118,7 +118,7 @@
|
||||
"description": "The duration (in seconds) that the user should be blocked from attempting to authenticate again.",
|
||||
"value": "60"
|
||||
},
|
||||
"LOG_CHANNEL": {
|
||||
"APP_LOG": {
|
||||
"description": "Driver to send logs to. (errorlog for stderr)",
|
||||
"value": "errorlog"
|
||||
},
|
||||
@@ -148,7 +148,7 @@
|
||||
"image": "heroku/php",
|
||||
"addons": [
|
||||
"cleardb:ignite",
|
||||
"heroku-redis:mini",
|
||||
"heroku-redis:hobby-dev",
|
||||
"papertrail:choklad"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class CheckoutLicenseToAllUsers extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Checks out licenses to all users';
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
@@ -56,7 +56,7 @@ class CheckoutLicenseToAllUsers extends Command
|
||||
return false;
|
||||
}
|
||||
|
||||
$users = User::whereNull('deleted_at')->where('autoassign_licenses', '=', 1)->with('licenses')->get();
|
||||
$users = User::whereNull('deleted_at')->with('licenses')->get();
|
||||
|
||||
if ($users->count() > $license->getAvailSeatsCountAttribute()) {
|
||||
$this->info('You do not have enough free seats to complete this task, so we will check out as many as we can. ');
|
||||
|
||||
@@ -3,31 +3,15 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use \App\Models\User;
|
||||
|
||||
|
||||
class CreateAdmin extends Command
|
||||
{
|
||||
|
||||
/** @mixin User **/
|
||||
/**
|
||||
* App\Console\CreateAdmin
|
||||
* @property mixed $first_name
|
||||
* @property string $last_name
|
||||
* @property string $username
|
||||
* @property string $email
|
||||
* @property string $permissions
|
||||
* @property string $password
|
||||
* @property boolean $activated
|
||||
* @property boolean $show_in_list
|
||||
* @property boolean $autoassign_licenses
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property mixed $created_by
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
|
||||
|
||||
|
||||
protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?} {autoassign_licenses?}';
|
||||
protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -46,7 +30,11 @@ class CreateAdmin extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$first_name = $this->option('first_name');
|
||||
@@ -55,14 +43,11 @@ class CreateAdmin extends Command
|
||||
$email = $this->option('email');
|
||||
$password = $this->option('password');
|
||||
$show_in_list = $this->argument('show_in_list');
|
||||
$autoassign_licenses = $this->argument('autoassign_licenses');
|
||||
|
||||
|
||||
|
||||
if (($first_name == '') || ($last_name == '') || ($username == '') || ($email == '') || ($password == '')) {
|
||||
$this->info('ERROR: All fields are required.');
|
||||
} else {
|
||||
$user = new User;
|
||||
$user = new \App\Models\User;
|
||||
$user->first_name = $first_name;
|
||||
$user->last_name = $last_name;
|
||||
$user->username = $username;
|
||||
@@ -74,11 +59,6 @@ class CreateAdmin extends Command
|
||||
if ($show_in_list == 'false') {
|
||||
$user->show_in_list = 0;
|
||||
}
|
||||
|
||||
if ($autoassign_licenses == 'false') {
|
||||
$user->autoassign_licenses = 0;
|
||||
}
|
||||
|
||||
if ($user->save()) {
|
||||
$this->info('New user created');
|
||||
$user->groups()->attach(1);
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
use Laravel\Passport\TokenRepository;
|
||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||
use DB;
|
||||
|
||||
class GeneratePersonalAccessToken extends Command
|
||||
{
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:make-api-key
|
||||
{--user_id= : The ID of the user to create the token for.}
|
||||
{--name= : The name of the new API token}
|
||||
{--key-only : Only return the value of the API key}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'This console command allows you to generate Personal API tokens to be used with the Snipe-IT JSON REST API on behalf of a user.';
|
||||
|
||||
|
||||
/**
|
||||
* The token repository implementation.
|
||||
*
|
||||
* @var \Laravel\Passport\TokenRepository
|
||||
*/
|
||||
protected $tokenRepository;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(TokenRepository $tokenRepository, ValidationFactory $validation)
|
||||
{
|
||||
$this->validation = $validation;
|
||||
$this->tokenRepository = $tokenRepository;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
$accessTokenName = $this->option('name');
|
||||
if ($accessTokenName=='') {
|
||||
$accessTokenName = 'CLI Auth Token';
|
||||
}
|
||||
|
||||
if ($this->option('user_id')=='') {
|
||||
return $this->error('ERROR: user_id cannot be blank.');
|
||||
}
|
||||
|
||||
if ($user = User::find($this->option('user_id'))) {
|
||||
|
||||
$createAccessToken = $user->createToken($accessTokenName)->accessToken;
|
||||
|
||||
if ($this->option('key-only')) {
|
||||
$this->info($createAccessToken);
|
||||
|
||||
} else {
|
||||
|
||||
$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()) {
|
||||
$this->info('API Token ID: '.$token->id);
|
||||
}
|
||||
|
||||
$this->info('API Token User: '.$user->present()->fullName.' ('.$user->username.')');
|
||||
$this->info('API Token Name: '.$accessTokenName);
|
||||
$this->info('API Token: '.$createAccessToken);
|
||||
}
|
||||
} else {
|
||||
return $this->error('ERROR: Invalid user. API key was not created.');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Department;
|
||||
use App\Models\Group;
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Ldap;
|
||||
@@ -18,7 +17,7 @@ class LdapSync extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=*} {--base_dn=} {--filter=} {--summary} {--json_summary}';
|
||||
protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=} {--base_dn=} {--summary} {--json_summary}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -44,29 +43,19 @@ class LdapSync extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
// If LDAP enabled isn't set to 1 (ldap_enabled!=1) then we should cut this short immediately without going any further
|
||||
if (Setting::getSettings()->ldap_enabled!='1') {
|
||||
$this->error('LDAP is not enabled. Aborting. See Settings > LDAP to enable it.');
|
||||
exit();
|
||||
}
|
||||
|
||||
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
|
||||
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
|
||||
$ldap_result_username = Setting::getSettings()->ldap_username_field;
|
||||
$ldap_result_last_name = Setting::getSettings()->ldap_lname_field;
|
||||
$ldap_result_first_name = Setting::getSettings()->ldap_fname_field;
|
||||
$ldap_result_active_flag = Setting::getSettings()->ldap_active_flag;
|
||||
|
||||
$ldap_result_active_flag = Setting::getSettings()->ldap_active_flag_field;
|
||||
$ldap_result_emp_num = Setting::getSettings()->ldap_emp_num;
|
||||
$ldap_result_email = Setting::getSettings()->ldap_email;
|
||||
$ldap_result_phone = Setting::getSettings()->ldap_phone_field;
|
||||
$ldap_result_jobtitle = Setting::getSettings()->ldap_jobtitle;
|
||||
$ldap_result_country = Setting::getSettings()->ldap_country;
|
||||
$ldap_result_location = Setting::getSettings()->ldap_location;
|
||||
$ldap_result_dept = Setting::getSettings()->ldap_dept;
|
||||
$ldap_result_manager = Setting::getSettings()->ldap_manager;
|
||||
$ldap_default_group = Setting::getSettings()->ldap_default_group;
|
||||
$search_base = Setting::getSettings()->ldap_base_dn;
|
||||
|
||||
try {
|
||||
$ldapconn = Ldap::connectToLdap();
|
||||
@@ -76,7 +65,7 @@ class LdapSync extends Command
|
||||
$json_summary = ['error' => true, 'error_message' => $e->getMessage(), 'summary' => []];
|
||||
$this->info(json_encode($json_summary));
|
||||
}
|
||||
Log::info($e);
|
||||
LOG::info($e);
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -84,70 +73,42 @@ class LdapSync extends Command
|
||||
$summary = [];
|
||||
|
||||
try {
|
||||
|
||||
/**
|
||||
* if a location ID has been specified, use that OU
|
||||
*/
|
||||
if ( $this->option('location_id') ) {
|
||||
|
||||
foreach($this->option('location_id') as $location_id){
|
||||
$location_ou = Location::where('id', '=', $location_id)->value('ldap_ou');
|
||||
$search_base = $location_ou;
|
||||
Log::debug('Importing users from specified location OU: \"'.$search_base.'\".');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* if a manual base DN has been specified, use that. Allow the Base DN to override
|
||||
* even if there's a location-based DN - if you picked it, you must have picked it for a reason.
|
||||
*/
|
||||
if ($this->option('base_dn') != '') {
|
||||
$search_base = $this->option('base_dn');
|
||||
Log::debug('Importing users from specified base DN: \"'.$search_base.'\".');
|
||||
}
|
||||
|
||||
/**
|
||||
* If a filter has been specified, use that
|
||||
*/
|
||||
if ($this->option('filter') != '') {
|
||||
$results = Ldap::findLdapUsers($search_base, -1, $this->option('filter'));
|
||||
LOG::debug('Importing users from specified base DN: \"'.$search_base.'\".');
|
||||
} else {
|
||||
$results = Ldap::findLdapUsers($search_base);
|
||||
$search_base = null;
|
||||
}
|
||||
|
||||
$results = Ldap::findLdapUsers($search_base);
|
||||
} catch (\Exception $e) {
|
||||
if ($this->option('json_summary')) {
|
||||
$json_summary = ['error' => true, 'error_message' => $e->getMessage(), 'summary' => []];
|
||||
$this->info(json_encode($json_summary));
|
||||
}
|
||||
Log::info($e);
|
||||
LOG::info($e);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/* Determine which location to assign users to by default. */
|
||||
$location = null; // TODO - this would be better called "$default_location", which is more explicit about its purpose
|
||||
|
||||
if ($this->option('location') != '') {
|
||||
if ($location = Location::where('name', '=', $this->option('location'))->first()) {
|
||||
Log::debug('Location name ' . $this->option('location') . ' passed');
|
||||
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')');
|
||||
}
|
||||
|
||||
} elseif ($this->option('location_id')) {
|
||||
foreach($this->option('location_id') as $location_id) {
|
||||
if ($location = Location::where('id', '=', $location_id)->first()) {
|
||||
Log::debug('Location ID ' . $location_id . ' passed');
|
||||
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')');
|
||||
}
|
||||
|
||||
}
|
||||
$location = Location::where('name', '=', $this->option('location'))->first();
|
||||
LOG::debug('Location name '.$this->option('location').' passed');
|
||||
LOG::debug('Importing to '.$location->name.' ('.$location->id.')');
|
||||
} elseif ($this->option('location_id') != '') {
|
||||
$location = Location::where('id', '=', $this->option('location_id'))->first();
|
||||
LOG::debug('Location ID '.$this->option('location_id').' passed');
|
||||
LOG::debug('Importing to '.$location->name.' ('.$location->id.')');
|
||||
}
|
||||
|
||||
if (! isset($location)) {
|
||||
Log::debug('That location is invalid or a location was not provided, so no location will be assigned by default.');
|
||||
LOG::debug('That location is invalid or a location was not provided, so no location will be assigned by default.');
|
||||
}
|
||||
|
||||
/* Process locations with explicitly defined OUs, if doing a full import. */
|
||||
if ($this->option('base_dn') == '' && $this->option('filter') == '') {
|
||||
if ($this->option('base_dn') == '') {
|
||||
// Retrieve locations with a mapped OU, and sort them from the shallowest to deepest OU (see #3993)
|
||||
$ldap_ou_locations = Location::where('ldap_ou', '!=', '')->get()->toArray();
|
||||
$ldap_ou_lengths = [];
|
||||
@@ -159,7 +120,7 @@ class LdapSync extends Command
|
||||
array_multisort($ldap_ou_lengths, SORT_ASC, $ldap_ou_locations);
|
||||
|
||||
if (count($ldap_ou_locations) > 0) {
|
||||
Log::debug('Some locations have special OUs set. Locations will be automatically set for users in those OUs.');
|
||||
LOG::debug('Some locations have special OUs set. Locations will be automatically set for users in those OUs.');
|
||||
}
|
||||
|
||||
// Inject location information fields
|
||||
@@ -177,7 +138,7 @@ class LdapSync extends Command
|
||||
$json_summary = ['error' => true, 'error_message' => trans('admin/users/message.error.ldap_could_not_search').' Location: '.$ldap_loc['name'].' (ID: '.$ldap_loc['id'].') cannot connect to "'.$ldap_loc['ldap_ou'].'" - '.$e->getMessage(), 'summary' => []];
|
||||
$this->info(json_encode($json_summary));
|
||||
}
|
||||
Log::info($e);
|
||||
LOG::info($e);
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -205,44 +166,30 @@ class LdapSync extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$manager_cache = [];
|
||||
|
||||
if($ldap_default_group != null) {
|
||||
|
||||
$default = Group::find($ldap_default_group);
|
||||
if (!$default) {
|
||||
$ldap_default_group = null; // un-set the default group if that group doesn't exist
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Create user account entries in Snipe-IT */
|
||||
$tmp_pass = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 20);
|
||||
$pass = bcrypt($tmp_pass);
|
||||
|
||||
for ($i = 0; $i < $results['count']; $i++) {
|
||||
if (empty($ldap_result_active_flag) || $results[$i][$ldap_result_active_flag][0] == 'TRUE') {
|
||||
$item = [];
|
||||
$item['username'] = $results[$i][$ldap_result_username][0] ?? '';
|
||||
$item['employee_number'] = $results[$i][$ldap_result_emp_num][0] ?? '';
|
||||
$item['lastname'] = $results[$i][$ldap_result_last_name][0] ?? '';
|
||||
$item['firstname'] = $results[$i][$ldap_result_first_name][0] ?? '';
|
||||
$item['email'] = $results[$i][$ldap_result_email][0] ?? '';
|
||||
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
|
||||
$item['location_id'] = $results[$i]['location_id'] ?? '';
|
||||
$item['telephone'] = $results[$i][$ldap_result_phone][0] ?? '';
|
||||
$item['jobtitle'] = $results[$i][$ldap_result_jobtitle][0] ?? '';
|
||||
$item['country'] = $results[$i][$ldap_result_country][0] ?? '';
|
||||
$item['department'] = $results[$i][$ldap_result_dept][0] ?? '';
|
||||
$item['manager'] = $results[$i][$ldap_result_manager][0] ?? '';
|
||||
$item['location'] = $results[$i][$ldap_result_location][0] ?? '';
|
||||
$item['username'] = isset($results[$i][$ldap_result_username][0]) ? $results[$i][$ldap_result_username][0] : '';
|
||||
$item['employee_number'] = isset($results[$i][$ldap_result_emp_num][0]) ? $results[$i][$ldap_result_emp_num][0] : '';
|
||||
$item['lastname'] = isset($results[$i][$ldap_result_last_name][0]) ? $results[$i][$ldap_result_last_name][0] : '';
|
||||
$item['firstname'] = isset($results[$i][$ldap_result_first_name][0]) ? $results[$i][$ldap_result_first_name][0] : '';
|
||||
$item['email'] = isset($results[$i][$ldap_result_email][0]) ? $results[$i][$ldap_result_email][0] : '';
|
||||
$item['ldap_location_override'] = isset($results[$i]['ldap_location_override']) ? $results[$i]['ldap_location_override'] : '';
|
||||
$item['location_id'] = isset($results[$i]['location_id']) ? $results[$i]['location_id'] : '';
|
||||
$item['telephone'] = isset($results[$i][$ldap_result_phone][0]) ? $results[$i][$ldap_result_phone][0] : '';
|
||||
$item['jobtitle'] = isset($results[$i][$ldap_result_jobtitle][0]) ? $results[$i][$ldap_result_jobtitle][0] : '';
|
||||
$item['country'] = isset($results[$i][$ldap_result_country][0]) ? $results[$i][$ldap_result_country][0] : '';
|
||||
$item['department'] = isset($results[$i][$ldap_result_dept][0]) ? $results[$i][$ldap_result_dept][0] : '';
|
||||
|
||||
// ONLY if you are using the "ldap_location" option *AND* you have an actual result
|
||||
if ($ldap_result_location && $item['location']) {
|
||||
$location = Location::firstOrCreate([
|
||||
'name' => $item['location'],
|
||||
]);
|
||||
}
|
||||
$department = Department::firstOrCreate([
|
||||
'name' => $item['department'],
|
||||
]);
|
||||
|
||||
|
||||
$user = User::where('username', $item['username'])->first();
|
||||
if ($user) {
|
||||
// Updating an existing user.
|
||||
@@ -250,101 +197,24 @@ class LdapSync extends Command
|
||||
} else {
|
||||
// Creating a new user.
|
||||
$user = new User;
|
||||
$user->password = $user->noPassword();
|
||||
$user->activated = 1; // newly created users can log in by default, unless AD's UAC is in use, or an active flag is set (below)
|
||||
$user->password = $pass;
|
||||
$user->activated = 0;
|
||||
$item['createorupdate'] = 'created';
|
||||
}
|
||||
|
||||
//If a sync option is not filled in on the LDAP settings don't populate the user field
|
||||
if($ldap_result_username != null){
|
||||
$user->username = $item['username'];
|
||||
}
|
||||
if($ldap_result_last_name != null){
|
||||
$user->last_name = $item['lastname'];
|
||||
}
|
||||
if($ldap_result_first_name != null){
|
||||
$user->first_name = $item['firstname'];
|
||||
}
|
||||
if($ldap_result_emp_num != null){
|
||||
$user->employee_num = e($item['employee_number']);
|
||||
}
|
||||
if($ldap_result_email != null){
|
||||
$user->last_name = $item['lastname'];
|
||||
$user->username = $item['username'];
|
||||
$user->email = $item['email'];
|
||||
}
|
||||
if($ldap_result_phone != null){
|
||||
$user->employee_num = e($item['employee_number']);
|
||||
$user->phone = $item['telephone'];
|
||||
}
|
||||
if($ldap_result_jobtitle != null){
|
||||
$user->jobtitle = $item['jobtitle'];
|
||||
}
|
||||
if($ldap_result_country != null){
|
||||
$user->country = $item['country'];
|
||||
}
|
||||
if($ldap_result_dept != null){
|
||||
$user->department_id = $department->id;
|
||||
}
|
||||
if($ldap_result_location != null){
|
||||
$user->location_id = $location ? $location->id : null;
|
||||
}
|
||||
|
||||
if($ldap_result_manager != null){
|
||||
if($item['manager'] != null) {
|
||||
// Check Cache first
|
||||
if (isset($manager_cache[$item['manager']])) {
|
||||
// found in cache; use that and avoid extra lookups
|
||||
$user->manager_id = $manager_cache[$item['manager']];
|
||||
} else {
|
||||
// Get the LDAP Manager
|
||||
try {
|
||||
$ldap_manager = Ldap::findLdapUsers($item['manager'], -1, $this->option('filter'));
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning("Manager lookup caused an exception: " . $e->getMessage() . ". Falling back to direct username lookup");
|
||||
// Hail-mary for Okta manager 'shortnames' - will only work if
|
||||
// Okta configuration is using full email-address-style usernames
|
||||
$ldap_manager = [
|
||||
"count" => 1,
|
||||
0 => [
|
||||
$ldap_result_username => [$item['manager']]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
if ($ldap_manager["count"] > 0) {
|
||||
|
||||
// Get the Manager's username
|
||||
// PHP LDAP returns every LDAP attribute as an array, and 90% of the time it's an array of just one item. But, hey, it's an array.
|
||||
$ldapManagerUsername = $ldap_manager[0][$ldap_result_username][0];
|
||||
|
||||
// Get User from Manager username.
|
||||
$ldap_manager = User::where('username', $ldapManagerUsername)->first();
|
||||
|
||||
if ($ldap_manager && isset($ldap_manager->id)) {
|
||||
// Link user to manager id.
|
||||
$user->manager_id = $ldap_manager->id;
|
||||
}
|
||||
}
|
||||
$manager_cache[$item['manager']] = $ldap_manager && isset($ldap_manager->id) ? $ldap_manager->id : null; // Store results in cache, even if 'failed'
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync activated state for Active Directory.
|
||||
if ( !empty($ldap_result_active_flag)) { // IF we have an 'active' flag set....
|
||||
// ....then *most* things that are truthy will activate the user. Anything falsey will deactivate them.
|
||||
// (Specifically, we don't handle a value of '0.0' correctly)
|
||||
$raw_value = @$results[$i][$ldap_result_active_flag][0];
|
||||
$filter_var = filter_var($raw_value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
$boolean_cast = (bool)$raw_value;
|
||||
|
||||
$user->activated = $filter_var ?? $boolean_cast; // if filter_var() was true or false, use that. If it's null, use the $boolean_cast
|
||||
|
||||
} elseif (array_key_exists('useraccountcontrol', $results[$i]) ) {
|
||||
// ....otherwise, (ie if no 'active' LDAP flag is defined), IF the UAC setting exists,
|
||||
// ....then use the UAC setting on the account to determine can-log-in vs. cannot-log-in
|
||||
|
||||
|
||||
/* The following is _probably_ the correct logic, but we can't use it because
|
||||
if (array_key_exists('useraccountcontrol', $results[$i])) {
|
||||
/* The following is _probably_ the correct logic, but we can't use it because
|
||||
some users may have been dependent upon the previous behavior, and this
|
||||
could cause additional access to be available to users they don't want
|
||||
to allow to log in.
|
||||
@@ -361,25 +231,25 @@ class LdapSync extends Command
|
||||
$user->activated = 0;
|
||||
} */
|
||||
$enabled_accounts = [
|
||||
'512', // 0x200 NORMAL_ACCOUNT
|
||||
'544', // 0x220 NORMAL_ACCOUNT, PASSWD_NOTREQD
|
||||
'66048', // 0x10200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD
|
||||
'66080', // 0x10220 NORMAL_ACCOUNT, PASSWD_NOTREQD, DONT_EXPIRE_PASSWORD
|
||||
'262656', // 0x40200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED
|
||||
'262688', // 0x40220 NORMAL_ACCOUNT, PASSWD_NOTREQD, SMARTCARD_REQUIRED
|
||||
'328192', // 0x50200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
|
||||
'328224', // 0x50220 NORMAL_ACCOUNT, PASSWD_NOT_REQD, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
|
||||
'4194816',// 0x400200 NORMAL_ACCOUNT, DONT_REQ_PREAUTH
|
||||
'512', // 0x200 NORMAL_ACCOUNT
|
||||
'544', // 0x220 NORMAL_ACCOUNT, PASSWD_NOTREQD
|
||||
'66048', // 0x10200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD
|
||||
'66080', // 0x10220 NORMAL_ACCOUNT, PASSWD_NOTREQD, DONT_EXPIRE_PASSWORD
|
||||
'262656', // 0x40200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED
|
||||
'262688', // 0x40220 NORMAL_ACCOUNT, PASSWD_NOTREQD, SMARTCARD_REQUIRED
|
||||
'328192', // 0x50200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
|
||||
'328224', // 0x50220 NORMAL_ACCOUNT, PASSWD_NOT_REQD, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
|
||||
'4194816',// 0x400200 NORMAL_ACCOUNT, DONT_REQ_PREAUTH
|
||||
'4260352', // 0x410200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, DONT_REQ_PREAUTH
|
||||
'1049088', // 0x100200 NORMAL_ACCOUNT, NOT_DELEGATED
|
||||
'1114624', // 0x110200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, NOT_DELEGATED,
|
||||
];
|
||||
$user->activated = (in_array($results[$i]['useraccountcontrol'][0], $enabled_accounts)) ? 1 : 0;
|
||||
}
|
||||
|
||||
// If we're not using AD, and there isn't an activated flag set, activate all users
|
||||
} /* implied 'else' here - leave the $user->activated flag alone. Newly-created accounts will be active.
|
||||
already-existing accounts will be however the administrator has set them */
|
||||
|
||||
elseif (empty($ldap_result_active_flag)) {
|
||||
$user->activated = 1;
|
||||
}
|
||||
|
||||
if ($item['ldap_location_override'] == true) {
|
||||
$user->location_id = $item['location_id'];
|
||||
@@ -398,10 +268,6 @@ class LdapSync extends Command
|
||||
if ($user->save()) {
|
||||
$item['note'] = $item['createorupdate'];
|
||||
$item['status'] = 'success';
|
||||
if ( $item['createorupdate'] === 'created' && $ldap_default_group) {
|
||||
$user->groups()->attach($ldap_default_group);
|
||||
}
|
||||
|
||||
} else {
|
||||
foreach ($user->getErrors()->getMessages() as $key => $err) {
|
||||
$errors .= $err[0];
|
||||
@@ -411,6 +277,7 @@ class LdapSync extends Command
|
||||
}
|
||||
|
||||
array_push($summary, $item);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->option('summary')) {
|
||||
|
||||
@@ -27,19 +27,6 @@ function ip_in_range( $ip, $range ) {
|
||||
}
|
||||
// NOTE - this function was shamelessly stolen from this gist: https://gist.github.com/tott/7684443
|
||||
|
||||
/**
|
||||
* Ensure LDAP filters are parentheses-wrapped
|
||||
*/
|
||||
function parenthesized_filter($filter)
|
||||
{
|
||||
if(substr($filter,0,1) == "(" ) {
|
||||
return $filter;
|
||||
} else {
|
||||
return "(".$filter.")";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LdapTroubleshooter extends Command
|
||||
{
|
||||
/**
|
||||
@@ -83,47 +70,6 @@ class LdapTroubleshooter extends Command
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the results from ldap_get_entries into something useful
|
||||
* @param array $array
|
||||
* @return array
|
||||
*/
|
||||
public function ldap_results_cleaner ($array) {
|
||||
$cleaned = [];
|
||||
for($i = 0; $i < $array['count']; $i++) {
|
||||
$row = $array[$i];
|
||||
$clean_row = [];
|
||||
foreach($row AS $key => $val ) {
|
||||
$this->debugout("Key is: ".$key);
|
||||
if($key == "count" || is_int($key) || $key == "dn") {
|
||||
$this->debugout(" and we're gonna skip it\n");
|
||||
continue;
|
||||
}
|
||||
$this->debugout(" And that seems fine.\n");
|
||||
if(array_key_exists('count',$val)) {
|
||||
if($val['count'] == 1) {
|
||||
$clean_row[$key] = $val[0];
|
||||
} else {
|
||||
unset($val['count']); //these counts are annoying
|
||||
$elements = [];
|
||||
foreach($val as $entry) {
|
||||
if(isset($ldap_constants[$entry])) {
|
||||
$elements[] = $ldap_constants[$entry];
|
||||
} else {
|
||||
$elements[] = $entry;
|
||||
}
|
||||
}
|
||||
$clean_row[$key] = $elements;
|
||||
}
|
||||
} else {
|
||||
$clean_row[$key] = $val;
|
||||
}
|
||||
}
|
||||
$cleaned[$i] = $clean_row;
|
||||
}
|
||||
return $cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
@@ -156,12 +102,16 @@ class LdapTroubleshooter extends Command
|
||||
$output[] = "LDAPTLS_KEY=storage/ldap_client_tls.key";
|
||||
}
|
||||
$output[] = "ldapsearch";
|
||||
$output[] = "-H ".$settings->ldap_server;
|
||||
$output[] = $settings->ldap_server;
|
||||
$output[] = "-x";
|
||||
$output[] = "-b ".escapeshellarg($settings->ldap_basedn);
|
||||
$output[] = "-D ".escapeshellarg($settings->ldap_uname);
|
||||
$output[] = "-w ".escapeshellarg(\Crypt::Decrypt($settings->ldap_pword));
|
||||
$output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter));
|
||||
if(substr($settings->ldap_filter,0,1) == "(" ) {
|
||||
$output[] = escapeshellarg($settings->ldap_filter);
|
||||
} else {
|
||||
$output[] = escapeshellarg("(".$settings->ldap_filter.")");
|
||||
}
|
||||
if($settings->ldap_tls) {
|
||||
$this->line("# adding STARTTLS option");
|
||||
$output[] = "-Z";
|
||||
@@ -340,8 +290,45 @@ class LdapTroubleshooter extends Command
|
||||
}
|
||||
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
|
||||
|
||||
// recursive function that 'cleans' the returned array from ldap_get_entries which are formatted awfully
|
||||
$cleaner = function ($array) {
|
||||
$cleaned = [];
|
||||
for($i = 0; $i < $array['count']; $i++) {
|
||||
$row = $array[$i];
|
||||
$clean_row = [];
|
||||
foreach($row AS $key => $val ) {
|
||||
$this->debugout("Key is: ".$key);
|
||||
if($key == "count" || is_int($key) || $key == "dn") {
|
||||
$this->debugout(" and we're gonna skip it\n");
|
||||
continue;
|
||||
}
|
||||
$this->debugout(" And that seems fine.\n");
|
||||
if(array_key_exists('count',$val)) {
|
||||
if($val['count'] == 1) {
|
||||
$clean_row[$key] = $val[0];
|
||||
} else {
|
||||
unset($val['count']); //these counts are annoying
|
||||
$elements = [];
|
||||
foreach($val as $entry) {
|
||||
if(isset($ldap_constants[$entry])) {
|
||||
$elements[] = $ldap_constants[$entry];
|
||||
} else {
|
||||
$elements[] = $entry;
|
||||
}
|
||||
}
|
||||
$clean_row[$key] = $elements;
|
||||
}
|
||||
} else {
|
||||
$clean_row[$key] = $val;
|
||||
}
|
||||
}
|
||||
$cleaned[$i] = $clean_row;
|
||||
}
|
||||
return $cleaned;
|
||||
};
|
||||
|
||||
foreach($ldap_urls AS $ldap_url) {
|
||||
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,Crypt::decrypt($settings->ldap_pword),$settings)) {
|
||||
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,Crypt::decrypt($settings->ldap_pword))) {
|
||||
$this->info("Success getting informational bind!");
|
||||
} else {
|
||||
$this->error("Unable to get information from bind.");
|
||||
@@ -435,9 +422,9 @@ class LdapTroubleshooter extends Command
|
||||
});
|
||||
}
|
||||
|
||||
public function test_informational_bind($ldap_url, $check_cert, $start_tls, $username, $password,$settings)
|
||||
public function test_informational_bind($ldap_url, $check_cert, $start_tls, $username, $password)
|
||||
{
|
||||
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls, $username, $password, $settings) {
|
||||
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls, $username, $password) {
|
||||
try { // TODO - copypasta'ed from test_authed_bind
|
||||
$conn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
|
||||
$bind_results = ldap_bind($conn, $username, $password);
|
||||
@@ -448,13 +435,12 @@ class LdapTroubleshooter extends Command
|
||||
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
|
||||
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
|
||||
$results = ldap_get_entries($conn, $result);
|
||||
$cleaned_results = $this->ldap_results_cleaner($results);
|
||||
$cleaned_results = $cleaner($results);
|
||||
$this->line(print_r($cleaned_results,true));
|
||||
//okay, great - now how do we display those results? I have no idea.
|
||||
// I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it?
|
||||
$this->comment("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
|
||||
$this->debugout("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->base_dn, $settings->filter);
|
||||
$this->info("Printing first 10 results: ");
|
||||
for($i=0;$i<10;$i++) {
|
||||
$this->info($search_results[$i]);
|
||||
|
||||
@@ -41,20 +41,10 @@ class MergeUsersByUsername extends Command
|
||||
{
|
||||
// Get the list of users who have an email address as their username
|
||||
$users = User::where('username', 'LIKE', '%@%')->whereNull('deleted_at')->get();
|
||||
$this->info($users->count().' total non-deleted users whose usernames contain a @ symbol.');
|
||||
|
||||
|
||||
foreach ($users as $user) {
|
||||
$parts = explode('@', trim($user->username));
|
||||
$this->info('Checking against username '.trim($parts[0]).'.');
|
||||
|
||||
|
||||
$bad_users = User::where('username', '=', trim($parts[0]))
|
||||
->whereNull('deleted_at')
|
||||
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations')
|
||||
->get();
|
||||
|
||||
|
||||
$parts = explode('@', $user->username);
|
||||
$bad_users = User::where('username', '=', $parts[0])->whereNull('deleted_at')->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations')->get();
|
||||
|
||||
foreach ($bad_users as $bad_user) {
|
||||
$this->info($bad_user->username.' ('.$bad_user->id.') will be merged into '.$user->username.' ('.$user->id.') ');
|
||||
|
||||
@@ -46,31 +46,30 @@ class MoveUploadsToNewDisk extends Command
|
||||
}
|
||||
$delete_local = $this->argument('delete_local');
|
||||
|
||||
$public_uploads['accessories'] = glob('public/uploads/accessories'."/*.*");
|
||||
$public_uploads['assets'] = glob('public/uploads/assets'."/*.*");
|
||||
$public_uploads['avatars'] = glob('public/uploads/avatars'."/*.*");
|
||||
$public_uploads['categories'] = glob('public/uploads/categories'."/*.*");
|
||||
$public_uploads['companies'] = glob('public/uploads/companies'."/*.*");
|
||||
$public_uploads['components'] = glob('public/uploads/components'."/*.*");
|
||||
$public_uploads['consumables'] = glob('public/uploads/consumables'."/*.*");
|
||||
$public_uploads['departments'] = glob('public/uploads/departments'."/*.*");
|
||||
$public_uploads['locations'] = glob('public/uploads/locations'."/*.*");
|
||||
$public_uploads['manufacturers'] = glob('public/uploads/manufacturers'."/*.*");
|
||||
$public_uploads['suppliers'] = glob('public/uploads/suppliers'."/*.*");
|
||||
$public_uploads['assetmodels'] = glob('public/uploads/models'."/*.*");
|
||||
|
||||
$public_uploads['accessories'] = glob('public/accessories'.'/*.*');
|
||||
$public_uploads['assets'] = glob('public/assets'.'/*.*');
|
||||
$public_uploads['avatars'] = glob('public/avatars'.'/*.*');
|
||||
$public_uploads['categories'] = glob('public/categories'.'/*.*');
|
||||
$public_uploads['companies'] = glob('public/companies'.'/*.*');
|
||||
$public_uploads['components'] = glob('public/components'.'/*.*');
|
||||
$public_uploads['consumables'] = glob('public/consumables'.'/*.*');
|
||||
$public_uploads['departments'] = glob('public/departments'.'/*.*');
|
||||
$public_uploads['locations'] = glob('public/locations'.'/*.*');
|
||||
$public_uploads['manufacturers'] = glob('public/manufacturers'.'/*.*');
|
||||
$public_uploads['suppliers'] = glob('public/suppliers'.'/*.*');
|
||||
$public_uploads['assetmodels'] = glob('public/models'.'/*.*');
|
||||
|
||||
// iterate files
|
||||
foreach ($public_uploads as $public_type => $public_upload) {
|
||||
$type_count = 0;
|
||||
$this->info('- There are ' . count($public_upload) . ' PUBLIC ' . $public_type . ' files.');
|
||||
$this->info('- There are '.count($public_upload).' PUBLIC '.$public_type.' files.');
|
||||
|
||||
for ($i = 0; $i < count($public_upload); $i++) {
|
||||
$type_count++;
|
||||
$filename = basename($public_upload[$i]);
|
||||
|
||||
try {
|
||||
Storage::disk('public')->put('uploads/'.$public_type.'/'.$filename, file_get_contents($public_upload[$i]));
|
||||
try {
|
||||
Storage::disk('public')->put('uploads/'.public_type.'/'.$filename, file_get_contents($public_upload[$i]));
|
||||
$new_url = Storage::disk('public')->url('uploads/'.$public_type.'/'.$filename, $filename);
|
||||
$this->info($type_count.'. PUBLIC: '.$filename.' was copied to '.$new_url);
|
||||
} catch (\Exception $e) {
|
||||
@@ -80,87 +79,83 @@ class MoveUploadsToNewDisk extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$logos = glob("public/uploads/setting*.*");
|
||||
$this->info("- There are ".count($logos).' files that might be logos.');
|
||||
$logos = glob('public/uploads/setting*.*');
|
||||
$this->info('- There are '.count($logos).' files that might be logos.');
|
||||
$type_count = 0;
|
||||
|
||||
foreach ($logos as $logo) {
|
||||
$this->info($logo);
|
||||
$type_count++;
|
||||
$filename = basename($logo);
|
||||
Storage::disk('public')->put('uploads/' . $filename, file_get_contents($logo));
|
||||
$this->info($type_count . '. LOGO: ' . $filename . ' was copied to ' . env('PUBLIC_AWS_URL') . '/uploads/' . $filename);
|
||||
Storage::disk('public')->put('uploads/'.$filename, file_get_contents($logo));
|
||||
$this->info($type_count.'. LOGO: '.$filename.' was copied to '.env('PUBLIC_AWS_URL').'/uploads/'.$filename);
|
||||
}
|
||||
|
||||
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
|
||||
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
|
||||
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
|
||||
$private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'."/*.*");
|
||||
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
|
||||
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
|
||||
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");
|
||||
$private_uploads['backups'] = glob('storage/private_uploads/backups'."/*.*");
|
||||
|
||||
$private_uploads['assets'] = glob('storage/private_uploads/assets'.'/*.*');
|
||||
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'.'/*.*');
|
||||
$private_uploads['audits'] = glob('storage/private_uploads/audits'.'/*.*');
|
||||
$private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'.'/*.*');
|
||||
$private_uploads['imports'] = glob('storage/private_uploads/imports'.'/*.*');
|
||||
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'.'/*.*');
|
||||
$private_uploads['users'] = glob('storage/private_uploads/users'.'/*.*');
|
||||
$private_uploads['backups'] = glob('storage/private_uploads/users'.'/*.*');
|
||||
|
||||
foreach ($private_uploads as $private_type => $private_upload) {
|
||||
{
|
||||
$this->info('- There are ' . count($private_upload) . ' PRIVATE ' . $private_type . ' files.');
|
||||
$this->info('- There are '.count($private_upload).' PRIVATE '.$private_type.' files.');
|
||||
|
||||
$type_count = 0;
|
||||
for ($x = 0; $x < count($private_upload); $x++) {
|
||||
$type_count++;
|
||||
$filename = basename($private_upload[$x]);
|
||||
$type_count = 0;
|
||||
for ($x = 0; $x < count($private_upload); $x++) {
|
||||
$type_count++;
|
||||
$filename = basename($private_upload[$x]);
|
||||
|
||||
try {
|
||||
Storage::put($private_type . '/' . $filename, file_get_contents($private_upload[$i]));
|
||||
$new_url = Storage::url($private_type . '/' . $filename, $filename);
|
||||
$this->info($type_count . '. PRIVATE: ' . $filename . ' was copied to ' . $new_url);
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug($e);
|
||||
$this->error($e);
|
||||
}
|
||||
try {
|
||||
Storage::put($private_type.'/'.$filename, file_get_contents($private_upload[$i]));
|
||||
$new_url = Storage::url($private_type.'/'.$filename, $filename);
|
||||
$this->info($type_count.'. PRIVATE: '.$filename.' was copied to '.$new_url);
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug($e);
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($delete_local == 'true') {
|
||||
$public_delete_count = 0;
|
||||
$private_delete_count = 0;
|
||||
|
||||
if ($delete_local == 'true') {
|
||||
$public_delete_count = 0;
|
||||
$private_delete_count = 0;
|
||||
$this->info("\n\n");
|
||||
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
$this->warn("\nTHIS WILL DELETE ALL OF YOUR LOCAL UPLOADED FILES. \n\nThis cannot be undone, so you should take a backup of your system before you proceed.\n");
|
||||
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
|
||||
$this->info("\n\n");
|
||||
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
$this->warn("\nTHIS WILL DELETE ALL OF YOUR LOCAL UPLOADED FILES. \n\nThis cannot be undone, so you should take a backup of your system before you proceed.\n");
|
||||
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
|
||||
|
||||
if ($this->confirm('Do you wish to continue?')) {
|
||||
foreach ($public_uploads as $public_type => $public_upload) {
|
||||
for ($i = 0; $i < count($public_upload); $i++) {
|
||||
$filename = $public_upload[$i];
|
||||
try {
|
||||
unlink($filename);
|
||||
$public_delete_count++;
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug($e);
|
||||
$this->error($e);
|
||||
}
|
||||
if ($this->confirm('Do you wish to continue?')) {
|
||||
foreach ($public_uploads as $public_type => $public_upload) {
|
||||
for ($i = 0; $i < count($public_upload); $i++) {
|
||||
$filename = $public_upload[$i];
|
||||
try {
|
||||
unlink($filename);
|
||||
$public_delete_count++;
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug($e);
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($private_uploads as $private_type => $private_upload) {
|
||||
for ($i = 0; $i < count($private_upload); $i++) {
|
||||
$filename = $private_upload[$i];
|
||||
try {
|
||||
unlink($filename);
|
||||
$private_delete_count++;
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug($e);
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->info($public_delete_count . ' PUBLIC local files and ' . $private_delete_count . ' PRIVATE local files were deleted from your filesystem.');
|
||||
}
|
||||
|
||||
foreach ($private_uploads as $private_type => $private_upload) {
|
||||
for ($i = 0; $i < count($private_upload); $i++) {
|
||||
$filename = $private_upload[$i];
|
||||
try {
|
||||
unlink($filename);
|
||||
$private_delete_count++;
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug($e);
|
||||
$this->error($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->info($public_delete_count.' PUBLIC local files and '.$private_delete_count.' PRIVATE local files were deleted from your filesystem.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\User;
|
||||
|
||||
class NormalizeUserNames extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:normalize-names';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Normalizes weirdly formatted names as first-letter upercased';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
$users = User::get();
|
||||
$this->info($users->count() . ' users');
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->first_name = ucwords(strtolower($user->first_name));
|
||||
$user->last_name = ucwords(strtolower($user->last_name));
|
||||
$user->email = strtolower($user->email);
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,31 +71,22 @@ class ReEncodeCustomFieldNames extends Command
|
||||
*/
|
||||
$last_part = substr(strrchr($asset_column, '_snipeit_'), 1);
|
||||
$custom_field_columns[$last_part] = $asset_column;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$this->info($field->name.' ('.$field->id.') column should be '.$field->convertUnicodeDbSlug());
|
||||
$this->info($field->name.' ('.$field->id.') column should be '.$field->convertUnicodeDbSlug().'');
|
||||
|
||||
/** The assets table has the column it should have, all is well */
|
||||
if ($field->db_column == $field->convertUnicodeDbSlug() && \Schema::hasColumn('assets', $field->convertUnicodeDbSlug())) {
|
||||
$this->info('-- ✓ This field exists on the assets table and the value for db_column matches in the custom_fields table.');
|
||||
if (\Schema::hasColumn('assets', $field->convertUnicodeDbSlug())) {
|
||||
$this->info('-- ✓ This field exists - all good');
|
||||
|
||||
/**
|
||||
* There is a mismatch between the fieldname on the assets table and
|
||||
* what $field->convertUnicodeDbSlug() is *now* expecting.
|
||||
*/
|
||||
} else {
|
||||
|
||||
if ($field->db_column != $field->convertUnicodeDbSlug()) {
|
||||
$this->error('-- ✘ Field mismatch: '.$field->name.' value should be '.$field->convertUnicodeDbSlug().' but is '.$field->db_column.' in the custom_fields table');
|
||||
|
||||
} else {
|
||||
$this->error('-- ✘ Field mismatch: '.$field->name.' column should be '.$field->convertUnicodeDbSlug().' but is '.$custom_field_columns[$field->id].' on the assets table.');
|
||||
|
||||
}
|
||||
|
||||
$this->warn('-- X Field mismatch: updating... ');
|
||||
|
||||
/** Make sure the custom_field_columns array has the ID */
|
||||
if (array_key_exists($field->id, $custom_field_columns)) {
|
||||
@@ -104,19 +95,13 @@ class ReEncodeCustomFieldNames extends Command
|
||||
* Update the asset schema to the corrected fieldname that will be recognized by the
|
||||
* system elsewhere that we use $field->convertUnicodeDbSlug()
|
||||
*/
|
||||
$this->info('-- ✓ Updating field from '.$field->db_column.' to '.$field->convertUnicodeDbSlug().' in the assets table');
|
||||
\Schema::table('assets', function ($table) use ($custom_field_columns, $field) {
|
||||
$table->renameColumn($custom_field_columns[$field->id], $field->convertUnicodeDbSlug());
|
||||
});
|
||||
|
||||
$this->info('-- ✓ Updating field from '.$field->db_column.' to '.$field->convertUnicodeDbSlug().' in the custom fields table');
|
||||
|
||||
$field->db_column = $field->convertUnicodeDbSlug();
|
||||
$field->save();
|
||||
|
||||
|
||||
$this->warn('-- ✓ Field updated from '.$custom_field_columns[$field->id].' to '.$field->convertUnicodeDbSlug());
|
||||
} else {
|
||||
$this->warn('-- ✘ WARNING: There is no field on the assets table ending in '.$field->id.'. This may require more in-depth investigation and may mean the schema was altered manually.');
|
||||
$this->warn('-- X WARNING: There is no field on the assets table ending in '.$field->id.'. This may require more in-depth investigation and may mean the schema was altered manually.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class RegenerateAssetTags extends Command
|
||||
}
|
||||
|
||||
foreach ($total_assets as $asset) {
|
||||
|
||||
$start_tag++;
|
||||
$output['info'][] = 'Asset tag:'.$asset->asset_tag;
|
||||
$asset->asset_tag = $settings->auto_increment_prefix.$settings->auto_increment_prefix.$start_tag;
|
||||
|
||||
@@ -72,15 +72,8 @@ class RegenerateAssetTags extends Command
|
||||
|
||||
// Use forceSave here to override model level validation
|
||||
$asset->forceSave();
|
||||
$start_tag++;
|
||||
if ($bar) {
|
||||
$bar->advance();
|
||||
}
|
||||
}
|
||||
|
||||
$settings->next_auto_tag_base = Asset::zerofill($start_tag, $settings->zerofill_count);
|
||||
$settings->save();
|
||||
|
||||
$bar->finish();
|
||||
$this->info("\n");
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ class ResetDemoSettings extends Command
|
||||
$settings->auto_increment_assets = 1;
|
||||
$settings->logo = 'snipe-logo.png';
|
||||
$settings->alert_email = 'service@snipe-it.io';
|
||||
$settings->login_note = 'Use `admin` / `password` to login to the demo.';
|
||||
$settings->header_color = null;
|
||||
$settings->barcode_type = 'QRCODE';
|
||||
$settings->default_currency = 'USD';
|
||||
@@ -63,7 +62,7 @@ class ResetDemoSettings extends Command
|
||||
$settings->date_display_format = 'D M d, Y';
|
||||
$settings->time_display_format = 'g:iA';
|
||||
$settings->thumbnail_max_h = '30';
|
||||
$settings->locale = 'en-US';
|
||||
$settings->locale = 'en';
|
||||
$settings->version_footer = 'on';
|
||||
$settings->support_footer = null;
|
||||
$settings->saml_enabled = '0';
|
||||
@@ -78,7 +77,7 @@ class ResetDemoSettings extends Command
|
||||
$settings->save();
|
||||
|
||||
if ($user = User::where('username', '=', 'admin')->first()) {
|
||||
$user->locale = 'en-US';
|
||||
$user->locale = 'en';
|
||||
$user->save();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,151 +5,6 @@ namespace App\Console\Commands;
|
||||
use Illuminate\Console\Command;
|
||||
use ZipArchive;
|
||||
|
||||
class SQLStreamer {
|
||||
private $input;
|
||||
private $output;
|
||||
// embed the prefix here?
|
||||
public ?string $prefix;
|
||||
|
||||
private bool $reading_beginning_of_line = true;
|
||||
|
||||
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
|
||||
|
||||
public array $tablenames = [];
|
||||
private bool $should_guess = false;
|
||||
private bool $statement_is_permitted = false;
|
||||
|
||||
public function __construct($input, $output, string $prefix = null)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
public function parse_sql(string $line): string {
|
||||
// take into account the 'start of line or not' setting as an instance variable?
|
||||
// 'continuation' lines for a permitted statement are PERMITTED.
|
||||
if($this->statement_is_permitted && $line[0] === ' ') {
|
||||
return $line;
|
||||
}
|
||||
|
||||
$table_regex = '`?([a-zA-Z0-9_]+)`?';
|
||||
$allowed_statements = [
|
||||
"/^(DROP TABLE (?:IF EXISTS )?)`$table_regex(.*)$/" => false,
|
||||
"/^(CREATE TABLE )$table_regex(.*)$/" => true, //sets up 'continuation'
|
||||
"/^(LOCK TABLES )$table_regex(.*)$/" => false,
|
||||
"/^(INSERT INTO )$table_regex(.*)$/" => false,
|
||||
"/^UNLOCK TABLES/" => false,
|
||||
// "/^\\) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;/" => false, // FIXME not sure what to do here?
|
||||
"/^\\)[a-zA-Z0-9_= ]*;$/" => false
|
||||
// ^^^^^^ that bit should *exit* the 'perimitted' black
|
||||
];
|
||||
|
||||
foreach($allowed_statements as $statement => $statechange) {
|
||||
// $this->info("Checking regex: $statement...\n");
|
||||
$matches = [];
|
||||
if (preg_match($statement,$line,$matches)) {
|
||||
$this->statement_is_permitted = $statechange;
|
||||
// matches are: 1 => first part of the statement, 2 => tablename, 3 => rest of statement
|
||||
// (with of course 0 being "the whole match")
|
||||
if (@$matches[2]) {
|
||||
// print "Found a tablename! It's: ".$matches[2]."\n";
|
||||
if ($this->should_guess) {
|
||||
@$this->tablenames[$matches[2]] += 1;
|
||||
continue; //oh? FIXME
|
||||
} else {
|
||||
$cleaned_tablename = \DB::getTablePrefix().preg_replace('/^'.$this->prefix.'/','',$matches[2]);
|
||||
$line = preg_replace($statement,'$1`'.$cleaned_tablename.'`$3' , $line);
|
||||
}
|
||||
} else {
|
||||
// no explicit tablename in this one, leave the line alone
|
||||
}
|
||||
//how do we *replace* the tablename?
|
||||
// print "RETURNING LINE: $line";
|
||||
return $line;
|
||||
}
|
||||
}
|
||||
// all that is not allowed is denied.
|
||||
return "";
|
||||
}
|
||||
|
||||
//this is used in exactly *TWO* places, and in both cases should return a prefix I think?
|
||||
// first - if you do the --sanitize-only one (which is mostly for testing/development)
|
||||
// next - when you run *without* a guessed prefix, this is run first to figure out the prefix
|
||||
// I think we have to *duplicate* the call to be able to run it again?
|
||||
public static function guess_prefix($input):string
|
||||
{
|
||||
$parser = new self($input, null);
|
||||
$parser->should_guess = true;
|
||||
$parser->line_aware_piping(); // <----- THIS is doing the heavy lifting!
|
||||
|
||||
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics?
|
||||
//can't use 'users' because the 'accessories_users' table?
|
||||
// can't use 'assets' because 'ver1_components_assets'
|
||||
foreach($check_tables as $check_table => $_ignore) {
|
||||
foreach ($parser->tablenames as $tablename => $_count) {
|
||||
// print "Comparing $tablename to $check_table\n";
|
||||
if (str_ends_with($tablename,$check_table)) {
|
||||
// print "Found one!\n";
|
||||
$check_tables[$check_table] = substr($tablename,0,-strlen($check_table));
|
||||
}
|
||||
}
|
||||
}
|
||||
$guessed_prefix = null;
|
||||
foreach ($check_tables as $clean_table => $prefix_guess) {
|
||||
if(is_null($prefix_guess)) {
|
||||
print("Couldn't find table $clean_table\n");
|
||||
die();
|
||||
}
|
||||
if(is_null($guessed_prefix)) {
|
||||
$guessed_prefix = $prefix_guess;
|
||||
} else {
|
||||
if ($guessed_prefix != $prefix_guess) {
|
||||
print("Prefix mismatch! Had guessed $guessed_prefix but got $prefix_guess\n");
|
||||
die();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $guessed_prefix;
|
||||
|
||||
}
|
||||
|
||||
public function line_aware_piping(): int
|
||||
{
|
||||
$bytes_read = 0;
|
||||
if (! $this->input) {
|
||||
throw new \Exception("No Input available for line_aware_piping");
|
||||
}
|
||||
|
||||
while (($buffer = fgets($this->input, SQLStreamer::$buffer_size)) !== false) {
|
||||
$bytes_read += strlen($buffer);
|
||||
if ($this->reading_beginning_of_line) {
|
||||
// \Log::debug("Buffer is: '$buffer'");
|
||||
$cleaned_buffer = $this->parse_sql($buffer);
|
||||
if ($this->output) {
|
||||
$bytes_written = fwrite($this->output, $cleaned_buffer);
|
||||
|
||||
if ($bytes_written === false) {
|
||||
throw new \Exception("Unable to write to pipe");
|
||||
}
|
||||
}
|
||||
}
|
||||
// if we got a newline at the end of this, then the _next_ read is the beginning of a line
|
||||
if($buffer[strlen($buffer)-1] === "\n") {
|
||||
$this->reading_beginning_of_line = true;
|
||||
} else {
|
||||
$this->reading_beginning_of_line = false;
|
||||
}
|
||||
|
||||
}
|
||||
return $bytes_read;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class RestoreFromBackup extends Command
|
||||
{
|
||||
/**
|
||||
@@ -157,13 +12,10 @@ class RestoreFromBackup extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
// FIXME - , stripping prefixes and nonstandard SQL statements. Without --prefix, guess and return the correct prefix to strip
|
||||
protected $signature = 'snipeit:restore
|
||||
{--force : Skip the danger prompt; assuming you enter "y"}
|
||||
{filename : The zip file to be migrated}
|
||||
{--no-progress : Don\'t show a progress bar}
|
||||
{--sanitize-guess-prefix : Guess and output the table-prefix needed to "sanitize" the SQL}
|
||||
{--sanitize-with-prefix= : "Sanitize" the SQL, using the passed-in table prefix (can be learned from --sanitize-guess-prefix). Pass as just \'--sanitize-with-prefix=\' to use no prefix}';
|
||||
{--force : Skip the danger prompt; assuming you hit "y"}
|
||||
{filename : The full path of the .zip file to be migrated}
|
||||
{--no-progress : Don\'t show a progress bar}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -182,6 +34,8 @@ class RestoreFromBackup extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
@@ -201,7 +55,7 @@ class RestoreFromBackup extends Command
|
||||
return $this->error('Missing required filename');
|
||||
}
|
||||
|
||||
if (! $this->option('force') && ! $this->option('sanitize-guess-prefix') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) {
|
||||
if (! $this->option('force') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) {
|
||||
return $this->error('Data loss not confirmed');
|
||||
}
|
||||
|
||||
@@ -230,36 +84,35 @@ class RestoreFromBackup extends Command
|
||||
|
||||
|
||||
$private_dirs = [
|
||||
'storage/private_uploads/accessories',
|
||||
'storage/private_uploads/assetmodels',
|
||||
'storage/private_uploads/assets', // these are asset _files_, not the pictures.
|
||||
'storage/private_uploads/audits',
|
||||
'storage/private_uploads/components',
|
||||
'storage/private_uploads/consumables',
|
||||
'storage/private_uploads/eula-pdfs',
|
||||
'storage/private_uploads/imports',
|
||||
'storage/private_uploads/assetmodels',
|
||||
'storage/private_uploads/users',
|
||||
'storage/private_uploads/licenses',
|
||||
'storage/private_uploads/signatures',
|
||||
'storage/private_uploads/users',
|
||||
];
|
||||
$private_files = [
|
||||
'storage/oauth-private.key',
|
||||
'storage/oauth-public.key',
|
||||
];
|
||||
$public_dirs = [
|
||||
'public/uploads/accessories',
|
||||
'public/uploads/assets', // these are asset _pictures_, not asset files
|
||||
'public/uploads/avatars',
|
||||
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
|
||||
'public/uploads/categories',
|
||||
'public/uploads/companies',
|
||||
'public/uploads/components',
|
||||
'public/uploads/categories',
|
||||
'public/uploads/manufacturers',
|
||||
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
|
||||
'public/uploads/consumables',
|
||||
'public/uploads/departments',
|
||||
'public/uploads/locations',
|
||||
'public/uploads/manufacturers',
|
||||
'public/uploads/models',
|
||||
'public/uploads/avatars',
|
||||
'public/uploads/suppliers',
|
||||
'public/uploads/assets', // these are asset _pictures_, not asset files
|
||||
'public/uploads/locations',
|
||||
'public/uploads/accessories',
|
||||
'public/uploads/models',
|
||||
'public/uploads/categories',
|
||||
'public/uploads/avatars',
|
||||
'public/uploads/manufacturers',
|
||||
];
|
||||
|
||||
$public_files = [
|
||||
@@ -296,7 +149,7 @@ class RestoreFromBackup extends Command
|
||||
$boring_files[] = $raw_path;
|
||||
continue;
|
||||
}
|
||||
if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') {
|
||||
if (@pathinfo($raw_path)['extension'] == 'sql') {
|
||||
\Log::debug("Found a sql file!");
|
||||
$sqlfiles[] = $raw_path;
|
||||
$sqlfile_indices[] = $i;
|
||||
@@ -304,11 +157,11 @@ class RestoreFromBackup extends Command
|
||||
}
|
||||
|
||||
foreach (array_merge($private_dirs, $public_dirs) as $dir) {
|
||||
$last_pos = strrpos($raw_path, $dir . '/');
|
||||
$last_pos = strrpos($raw_path, $dir.'/');
|
||||
if ($last_pos !== false) {
|
||||
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
|
||||
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
|
||||
$interesting_files[$raw_path] = ['dest' => $dir, 'index' => $i];
|
||||
$interesting_files[$raw_path] = ['dest' =>$dir, 'index' => $i];
|
||||
continue 2;
|
||||
if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) {
|
||||
// we don't care about that; we just want files with the appropriate prefix
|
||||
@@ -317,7 +170,7 @@ class RestoreFromBackup extends Command
|
||||
}
|
||||
}
|
||||
$good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
|
||||
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico',];
|
||||
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico', ];
|
||||
foreach (array_merge($private_files, $public_files) as $file) {
|
||||
$has_wildcard = (strpos($file, '*') !== false);
|
||||
if ($has_wildcard) {
|
||||
@@ -326,8 +179,8 @@ class RestoreFromBackup extends Command
|
||||
$last_pos = strrpos($raw_path, $file); // no trailing slash!
|
||||
if ($last_pos !== false) {
|
||||
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, $good_extensions)) {
|
||||
$this->warn('Potentially unsafe file ' . $raw_path . ' is being skipped');
|
||||
if (! in_array($extension, $good_extensions)) {
|
||||
$this->warn('Potentially unsafe file '.$raw_path.' is being skipped');
|
||||
$boring_files[] = $raw_path;
|
||||
continue 2;
|
||||
}
|
||||
@@ -342,6 +195,7 @@ class RestoreFromBackup extends Command
|
||||
}
|
||||
$boring_files[] = $raw_path; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file
|
||||
} // end of pre-processing the ZIP file for-loop
|
||||
|
||||
// print_r($interesting_files);exit(-1);
|
||||
|
||||
if (count($sqlfiles) != 1) {
|
||||
@@ -353,17 +207,6 @@ class RestoreFromBackup extends Command
|
||||
//older Snipe-IT installs don't have the db-dumps subdirectory component
|
||||
}
|
||||
|
||||
$sql_stat = $za->statIndex($sqlfile_indices[0]);
|
||||
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
|
||||
$sql_contents = $za->getStream($sql_stat['name']); // maybe copy *THIS* thing?
|
||||
|
||||
// OKAY, now that we *found* the sql file if we're doing just the guess-prefix thing, we can do that *HERE* I think?
|
||||
if ($this->option('sanitize-guess-prefix')) {
|
||||
$prefix = SQLStreamer::guess_prefix($sql_contents);
|
||||
$this->line($prefix);
|
||||
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitze your SQL.");
|
||||
}
|
||||
|
||||
//how to invoke the restore?
|
||||
$pipes = [];
|
||||
|
||||
@@ -371,7 +214,7 @@ class RestoreFromBackup extends Command
|
||||
$env_vars['MYSQL_PWD'] = config('database.connections.mysql.password');
|
||||
// TODO notes: we are stealing the dump_binary_path (which *probably* also has your copy of the mysql binary in it. But it might not, so we might need to extend this)
|
||||
// we unilaterally prepend a slash to the `mysql` command. This might mean your path could look like /blah/blah/blah//mysql - which should be fine. But maybe in some environments it isn't?
|
||||
$mysql_binary = config('database.connections.mysql.dump.dump_binary_path').\DIRECTORY_SEPARATOR.'mysql'.(\DIRECTORY_SEPARATOR == '\\' ? ".exe" : "");
|
||||
$mysql_binary = config('database.connections.mysql.dump.dump_binary_path').'/mysql';
|
||||
if( ! file_exists($mysql_binary) ) {
|
||||
return $this->error("mysql tool at: '$mysql_binary' does not exist, cannot restore. Please edit DB_DUMP_PATH in your .env to point to a directory that contains the mysqldump and mysql binary");
|
||||
}
|
||||
@@ -384,10 +227,6 @@ class RestoreFromBackup extends Command
|
||||
return $this->error('Unable to invoke mysql via CLI');
|
||||
}
|
||||
|
||||
// I'm not sure about these?
|
||||
stream_set_blocking($pipes[1], false); // use non-blocking reads for stdout
|
||||
stream_set_blocking($pipes[2], false); // use non-blocking reads for stderr
|
||||
|
||||
// $this->info("Stdout says? ".fgets($pipes[1])); //FIXME: I think we might need to set non-blocking mode to use this properly?
|
||||
// $this->info("Stderr says? ".fgets($pipes[2])); //FIXME: ditto, same.
|
||||
// should we read stdout?
|
||||
@@ -395,9 +234,9 @@ class RestoreFromBackup extends Command
|
||||
|
||||
//$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy!
|
||||
|
||||
// FIXME - this feels like it wants to go somewhere else?
|
||||
// and it doesn't seem 'right' - if you can't get a stream to the .sql file,
|
||||
// why do we care what's happening with pipes and stdout and stderr?!
|
||||
$sql_stat = $za->statIndex($sqlfile_indices[0]);
|
||||
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
|
||||
$sql_contents = $za->getStream($sql_stat['name']);
|
||||
if ($sql_contents === false) {
|
||||
$stdout = fgets($pipes[1]);
|
||||
$this->info($stdout);
|
||||
@@ -406,35 +245,22 @@ class RestoreFromBackup extends Command
|
||||
|
||||
return false;
|
||||
}
|
||||
$bytes_read = 0;
|
||||
|
||||
try {
|
||||
if ( $this->option('sanitize-with-prefix') === null) {
|
||||
// "Legacy" direct-piping
|
||||
$bytes_read = 0;
|
||||
while (($buffer = fgets($sql_contents, SQLStreamer::$buffer_size)) !== false) {
|
||||
$bytes_read += strlen($buffer);
|
||||
// \Log::debug("Buffer is: '$buffer'");
|
||||
$bytes_written = fwrite($pipes[0], $buffer);
|
||||
while (($buffer = fgets($sql_contents, self::$buffer_size)) !== false) {
|
||||
$bytes_read += strlen($buffer);
|
||||
// \Log::debug("Buffer is: '$buffer'");
|
||||
$bytes_written = fwrite($pipes[0], $buffer);
|
||||
|
||||
if ($bytes_written === false) {
|
||||
throw new Exception("Unable to write to pipe");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$sql_importer = new SQLStreamer($sql_contents, $pipes[0], $this->option('sanitize-with-prefix'));
|
||||
$bytes_read = $sql_importer->line_aware_piping();
|
||||
if ($bytes_written === false) {
|
||||
$stdout = fgets($pipes[1]);
|
||||
$this->info($stdout);
|
||||
$stderr = fgets($pipes[2]);
|
||||
$this->info($stderr);
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("Error during restore!!!! ".$e->getMessage());
|
||||
// FIXME - put these back and/or put them in the right places?!
|
||||
$err_out = fgets($pipes[1]);
|
||||
$err_err = fgets($pipes[2]);
|
||||
\Log::error("Error OUTPUT: ".$err_out);
|
||||
$this->info($err_out);
|
||||
\Log::error("Error ERROR : ".$err_err);
|
||||
$this->error($err_err);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if (!feof($sql_contents) || $bytes_read == 0) {
|
||||
return $this->error("Not at end of file for sql file, or zero bytes read. aborting!");
|
||||
}
|
||||
@@ -466,7 +292,7 @@ class RestoreFromBackup extends Command
|
||||
$fp = $za->getStream($ugly_file_name);
|
||||
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
|
||||
$migrated_file = fopen($file_details['dest'].'/'.basename($pretty_file_name), 'w');
|
||||
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
|
||||
while (($buffer = fgets($fp, self::$buffer_size)) !== false) {
|
||||
fwrite($migrated_file, $buffer);
|
||||
}
|
||||
fclose($migrated_file);
|
||||
|
||||
@@ -7,7 +7,6 @@ use App\Models\CustomField;
|
||||
use App\Models\Setting;
|
||||
use Artisan;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
|
||||
class RotateAppKey extends Command
|
||||
@@ -17,17 +16,14 @@ class RotateAppKey extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:rotate-key
|
||||
{previous_key? : The previous key to rotate from}
|
||||
{--emergency : Emergency mode - rotate from .env APP_KEY to newly-generated one, modifying .env}
|
||||
{--force : Skip interactive confirmation}';
|
||||
protected $signature = 'snipeit:rotate-key';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Rotates APP_KEY to a new value, optionally taking the previous key as an argument';
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
@@ -46,42 +42,26 @@ class RotateAppKey extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//make sure they specify only exactly one of --emergency, or a filename. Not neither, and not both.
|
||||
if ( (!$this->option('emergency') && !$this->argument('previous_key')) || ( $this->option('emergency') && $this->argument('previous_key'))) {
|
||||
$this->error("Specify only one of --emergency, or an app key value, in order to rotate keys");
|
||||
return 1;
|
||||
}
|
||||
if ( $this->option('emergency') ) {
|
||||
$msg = "\n****************************************************\nTHIS WILL MODIFY YOUR APP_KEY AND DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND \nRE-ENCRYPT THEM WITH A NEWLY GENERATED KEY. \n\nThere is NO undo. \n\nMake SURE you have a database backup and a backup of your .env generated BEFORE running this command. \n\nIf you do not save the newly generated APP_KEY to your .env in this process, \nyour encrypted data will no longer be decryptable. \n\nAre you SURE you wish to continue, and have confirmed you have a database backup and an .env backup? ";
|
||||
} else {
|
||||
$msg = "\n****************************************************\nTHIS WILL DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND RE-ENCRYPT THEM WITH YOUR\nAPP_KEY.\n\nThere is NO undo. \n\nMake SURE you have a database backup BEFORE running this command. \n\nAre you SURE you wish to continue, and have confirmed you have a database backup? ";
|
||||
}
|
||||
if ($this->option('force') || $this->confirm($msg)) {
|
||||
if ($this->confirm("\n****************************************************\nTHIS WILL MODIFY YOUR APP_KEY AND DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND \nRE-ENCRYPT THEM WITH A NEWLY GENERATED KEY. \n\nThere is NO undo. \n\nMake SURE you have a database backup and a backup of your .env generated BEFORE running this command. \n\nIf you do not save the newly generated APP_KEY to your .env in this process, \nyour encrypted data will no longer be decryptable. \n\nAre you SURE you wish to continue, and have confirmed you have a database backup and an .env backup? ")) {
|
||||
|
||||
// Get the existing app_key and ciphers
|
||||
// We put them in a variable since we clear the cache partway through here.
|
||||
if ($this->option('emergency')) {
|
||||
$old_app_key = config('app.key');
|
||||
$cipher = config('app.cipher');
|
||||
$old_app_key = config('app.key');
|
||||
$cipher = config('app.cipher');
|
||||
|
||||
// Generate a new one
|
||||
Artisan::call('key:generate', ['--show' => true]);
|
||||
$new_app_key = trim(Artisan::output());
|
||||
// Generate a new one
|
||||
Artisan::call('key:generate', ['--show' => true]);
|
||||
$new_app_key = Artisan::output();
|
||||
|
||||
// Clear the config cache
|
||||
Artisan::call('config:clear');
|
||||
// Clear the config cache
|
||||
Artisan::call('config:clear');
|
||||
|
||||
// Write the new app key to the .env file
|
||||
$this->writeNewEnvironmentFileWith($new_app_key);
|
||||
} elseif ($this->argument('previous_key')) {
|
||||
$old_app_key = $this->argument('previous_key');
|
||||
$cipher = config('app.cipher'); // just a guess?
|
||||
$new_app_key = config('app.key');
|
||||
}
|
||||
$this->warn('Your app cipher is: '.$cipher);
|
||||
$this->warn('Your old APP_KEY is: '.$old_app_key);
|
||||
$this->warn('Your new APP_KEY is: '.$new_app_key);
|
||||
|
||||
$this->warn('Your app cipher is: ' . $cipher);
|
||||
$this->warn('Your old APP_KEY is: ' . $old_app_key);
|
||||
$this->warn('Your new APP_KEY is: ' . $new_app_key);
|
||||
// Write the new app key to the .env file
|
||||
$this->writeNewEnvironmentFileWith($new_app_key);
|
||||
|
||||
// Manually create an old encrypter instance using the old app key
|
||||
// and also create a new encrypter instance so we can re-crypt the field
|
||||
@@ -95,16 +75,8 @@ class RotateAppKey extends Command
|
||||
$assets = Asset::whereNotNull($field->db_column)->get();
|
||||
|
||||
foreach ($assets as $asset) {
|
||||
try {
|
||||
$asset->{$field->db_column} = $oldEncrypter->decrypt($asset->{$field->db_column});
|
||||
$this->line('DECRYPTED: ' . $field->db_column);
|
||||
} catch (DecryptException $e) {
|
||||
$this->line('Could not decrypt '. $field->db_column.' using "old key" - skipping...');
|
||||
continue;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Error decrypting ".$field->db_column.", reason: ".$e->getMessage().". Aborting key rotation");
|
||||
throw $e;
|
||||
}
|
||||
$asset->{$field->db_column} = $oldEncrypter->decrypt($asset->{$field->db_column});
|
||||
$this->line('DECRYPTED: '.$field->db_column);
|
||||
$asset->{$field->db_column} = $newEncrypter->encrypt($asset->{$field->db_column});
|
||||
$this->line('ENCRYPTED: '.$field->db_column);
|
||||
$asset->save();
|
||||
@@ -114,14 +86,10 @@ class RotateAppKey extends Command
|
||||
// Handle the LDAP password if one is provided
|
||||
$setting = Setting::first();
|
||||
if ($setting->ldap_pword != '') {
|
||||
try {
|
||||
$setting->ldap_pword = $oldEncrypter->decrypt($setting->ldap_pword);
|
||||
$setting->ldap_pword = $newEncrypter->encrypt($setting->ldap_pword);
|
||||
$setting->save();
|
||||
$this->warn('LDAP password has been re-encrypted.');
|
||||
} catch(DecryptException $e) {
|
||||
$this->warn("Unable to decrypt old LDAP password; skipping");
|
||||
}
|
||||
$setting->ldap_pword = $oldEncrypter->decrypt($setting->ldap_pword);
|
||||
$setting->ldap_pword = $newEncrypter->encrypt($setting->ldap_pword);
|
||||
$setting->save();
|
||||
$this->warn('LDAP password has been re-encrypted.');
|
||||
}
|
||||
} else {
|
||||
$this->info('This operation has been canceled. No changes have been made.');
|
||||
@@ -138,7 +106,7 @@ class RotateAppKey extends Command
|
||||
{
|
||||
file_put_contents($this->laravel->environmentFilePath(), preg_replace(
|
||||
$this->keyReplacementPattern(),
|
||||
'APP_KEY="'.$key.'"',
|
||||
'APP_KEY='.$key,
|
||||
file_get_contents($this->laravel->environmentFilePath())
|
||||
));
|
||||
}
|
||||
@@ -150,7 +118,7 @@ class RotateAppKey extends Command
|
||||
*/
|
||||
protected function keyReplacementPattern()
|
||||
{
|
||||
$escaped = '="?'.preg_quote($this->laravel['config']['app.key'], '/').'"?';
|
||||
$escaped = preg_quote('='.$this->laravel['config']['app.key'], '/');
|
||||
|
||||
return "/^APP_KEY{$escaped}/m";
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\SamlNonce;
|
||||
|
||||
class SamlClearExpiredNonces extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'saml:clear_expired_nonces';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Clears out expired SAML assertions from the saml_nonces table';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
SamlNonce::where('not_valid_after','<=',now())->delete();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ use App\Notifications\ExpectedCheckinAdminNotification;
|
||||
use App\Notifications\ExpectedCheckinNotification;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SendExpectedCheckinAlerts extends Command
|
||||
{
|
||||
@@ -43,7 +42,7 @@ class SendExpectedCheckinAlerts extends Command
|
||||
public function handle()
|
||||
{
|
||||
$settings = Setting::getSettings();
|
||||
$whenNotify = Carbon::now();
|
||||
$whenNotify = Carbon::now()->addDays(7);
|
||||
$assets = Asset::with('assignedTo')->whereNotNull('assigned_to')->whereNotNull('expected_checkin')->where('expected_checkin', '<=', $whenNotify)->get();
|
||||
|
||||
$this->info($whenNotify.' is deadline');
|
||||
@@ -51,7 +50,6 @@ class SendExpectedCheckinAlerts extends Command
|
||||
|
||||
foreach ($assets as $asset) {
|
||||
if ($asset->assigned && $asset->checkedOutToUser()) {
|
||||
Log::info('Sending ExpectedCheckinNotification to ' . $asset->assigned->email);
|
||||
$asset->assigned->notify((new ExpectedCheckinNotification($asset)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,39 +39,33 @@ class SyncAssetCounters extends Command
|
||||
public function handle()
|
||||
{
|
||||
$start = microtime(true);
|
||||
|
||||
// We need the whole count of all assets in order to set up the progress bar
|
||||
$assets_count = Asset::withTrashed()->count();
|
||||
$bar = $this->output->createProgressBar($assets_count);
|
||||
|
||||
$assets = Asset::withCount('checkins as checkins_count', 'checkouts as checkouts_count', 'userRequests as user_requests_count')
|
||||
->withTrashed()->chunk(100, function ($assets) use ($bar) {
|
||||
->withTrashed()->get();
|
||||
|
||||
if ($assets->count() > 0) {
|
||||
if ($assets) {
|
||||
if ($assets->count() > 0) {
|
||||
$bar = $this->output->createProgressBar($assets->count());
|
||||
|
||||
foreach ($assets as $asset) {
|
||||
foreach ($assets as $asset) {
|
||||
$asset->checkin_counter = (int) $asset->checkins_count;
|
||||
$asset->checkout_counter = (int) $asset->checkouts_count;
|
||||
$asset->requests_counter = (int) $asset->user_requests_count;
|
||||
$asset->unsetEventDispatcher();
|
||||
$asset->save();
|
||||
$output['info'][] = 'Asset: '.$asset->id.' has '.$asset->checkin_counter.' checkins, '.$asset->checkout_counter.' checkouts, and '.$asset->requests_counter.' requests';
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
|
||||
$asset->checkin_counter = (int) $asset->checkins_count;
|
||||
$asset->checkout_counter = (int) $asset->checkouts_count;
|
||||
$asset->requests_counter = (int) $asset->user_requests_count;
|
||||
$asset->unsetEventDispatcher();
|
||||
$asset->save();
|
||||
$bar->advance();
|
||||
|
||||
\Log::debug('Asset: '.$asset->id.' has '.$asset->checkin_counter.' checkins, '.$asset->checkout_counter.' checkouts, and '.$asset->requests_counter.' requests');
|
||||
|
||||
}
|
||||
foreach ($output['info'] as $key => $output_text) {
|
||||
$this->info($output_text);
|
||||
}
|
||||
|
||||
$time_elapsed_secs = microtime(true) - $start;
|
||||
$this->info('Sync executed in '.$time_elapsed_secs.' seconds');
|
||||
} else {
|
||||
$this->info('No assets to sync');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$bar->finish();
|
||||
$time_elapsed_secs = microtime(true) - $start;
|
||||
$this->info("\nSync of ".$assets_count.' assets executed in '.$time_elapsed_secs.' seconds');
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class SystemBackup extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:backup {--filename=}';
|
||||
protected $name = 'snipeit:backup';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -37,18 +37,7 @@ class SystemBackup extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if ($this->option('filename')) {
|
||||
$filename = $this->option('filename');
|
||||
|
||||
// Make sure the filename ends in .zip
|
||||
if (!ends_with($filename, '.zip')) {
|
||||
$filename = $filename.'.zip';
|
||||
}
|
||||
|
||||
$this->call('backup:run', ['--filename' => $filename]);
|
||||
} else {
|
||||
$this->call('backup:run');
|
||||
}
|
||||
|
||||
//
|
||||
$this->call('backup:run');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\CustomField;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ToggleCustomfieldEncryption extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'snipeit:customfield-encryption
|
||||
{fieldname : the db_column_name of the field}';
|
||||
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'This command should be used to convert an unencrypted custom field into a custom field and encrypt the associated data in the assets table for that column.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$fieldname = $this->argument('fieldname');
|
||||
|
||||
if ($field = CustomField::where('db_column', $fieldname)->first()) {
|
||||
|
||||
// If the field is not encrypted, make it encrypted and encrypt the data in the assets table for the
|
||||
// corresponding field.
|
||||
DB::transaction(function () use ($field) {
|
||||
|
||||
if ($field->field_encrypted == 0) {
|
||||
$assets = Asset::whereNotNull($field->db_column)->get();
|
||||
|
||||
foreach ($assets as $asset) {
|
||||
$asset->{$field->db_column} = encrypt($asset->{$field->db_column});
|
||||
$asset->save();
|
||||
}
|
||||
|
||||
$field->field_encrypted = 1;
|
||||
$field->save();
|
||||
|
||||
// This field is already encrypted. Do nothing.
|
||||
} else {
|
||||
$this->error('The custom field ' . $field->db_column.' is already encrypted. No action was taken.');
|
||||
}
|
||||
});
|
||||
|
||||
// No matching column name found
|
||||
} else {
|
||||
$this->error('No matching results for unencrypted custom fields with db_column name: ' . $fieldname.'. Please check the fieldname.');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,6 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->command('snipeit:backup')->weekly();
|
||||
$schedule->command('backup:clean')->daily();
|
||||
$schedule->command('snipeit:upcoming-audits')->daily();
|
||||
$schedule->command('auth:clear-resets')->everyFifteenMinutes();
|
||||
$schedule->command('saml:clear_expired_nonces')->weekly();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,20 +15,18 @@ class CheckoutableCheckedIn
|
||||
public $checkedInBy;
|
||||
public $note;
|
||||
public $action_date; // Date setted in the hardware.checkin view at the checkin_at input, for the action log
|
||||
public $originalValues;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($checkoutable, $checkedOutTo, User $checkedInBy, $note, $action_date = null, $originalValues = [])
|
||||
public function __construct($checkoutable, $checkedOutTo, User $checkedInBy, $note, $action_date = null)
|
||||
{
|
||||
$this->checkoutable = $checkoutable;
|
||||
$this->checkedOutTo = $checkedOutTo;
|
||||
$this->checkedInBy = $checkedInBy;
|
||||
$this->note = $note;
|
||||
$this->action_date = $action_date ?? date('Y-m-d');
|
||||
$this->originalValues = $originalValues;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,17 @@ class CheckoutableCheckedOut
|
||||
public $checkedOutTo;
|
||||
public $checkedOutBy;
|
||||
public $note;
|
||||
public $originalValues;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = [])
|
||||
public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note)
|
||||
{
|
||||
$this->checkoutable = $checkoutable;
|
||||
$this->checkedOutTo = $checkedOutTo;
|
||||
$this->checkedOutBy = $checkedOutBy;
|
||||
$this->note = $note;
|
||||
$this->originalValues = $originalValues;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\User;
|
||||
|
||||
class UserMerged
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(User $from_user, User $to_user, User $admin)
|
||||
{
|
||||
$this->merged_from = $from_user;
|
||||
$this->merged_to = $to_user;
|
||||
$this->admin = $admin;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,10 @@ use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use App\Helpers\Helper;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use ArieTimmerman\Laravel\SCIMServer\Exceptions\SCIMException;
|
||||
use Log;
|
||||
use Throwable;
|
||||
use JsonException;
|
||||
use Carbon\Exceptions\InvalidFormatException;
|
||||
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
@@ -29,8 +28,6 @@ class Handler extends ExceptionHandler
|
||||
\Intervention\Image\Exception\NotSupportedException::class,
|
||||
\League\OAuth2\Server\Exception\OAuthServerException::class,
|
||||
JsonException::class,
|
||||
SCIMException::class, //these generally don't need to be reported
|
||||
InvalidFormatException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -44,9 +41,7 @@ class Handler extends ExceptionHandler
|
||||
public function report(Throwable $exception)
|
||||
{
|
||||
if ($this->shouldReport($exception)) {
|
||||
if (class_exists(\Log::class)) {
|
||||
\Log::error($exception);
|
||||
}
|
||||
Log::error($exception);
|
||||
return parent::report($exception);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +51,7 @@ class Handler extends ExceptionHandler
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $e
|
||||
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse|\Illuminate\Http\Response
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function render($request, Throwable $e)
|
||||
{
|
||||
@@ -70,39 +65,18 @@ class Handler extends ExceptionHandler
|
||||
// Invalid JSON exception
|
||||
// TODO: don't understand why we have to do this when we have the invalidJson() method, below, but, well, whatever
|
||||
if ($e instanceof JsonException) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Invalid JSON'), 422);
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'invalid JSON'), 422);
|
||||
}
|
||||
|
||||
// Handle SCIM exceptions
|
||||
if ($e instanceof SCIMException) {
|
||||
try {
|
||||
$e->report(); // logs as 'debug', so shouldn't get too noisy
|
||||
} catch(\Exception $reportException) {
|
||||
//do nothing
|
||||
}
|
||||
return $e->render($request); // ALL SCIMExceptions have the 'render()' method
|
||||
}
|
||||
|
||||
// Handle standard requests that fail because Carbon cannot parse the date on validation (when a submitted date value is definitely not a date)
|
||||
if ($e instanceof InvalidFormatException) {
|
||||
return redirect()->back()->withInput()->with('error', trans('validation.date', ['attribute' => 'date']));
|
||||
}
|
||||
|
||||
// Handle API requests that fail
|
||||
// Handle Ajax requests that fail because the model doesn't exist
|
||||
if ($request->ajax() || $request->wantsJson()) {
|
||||
|
||||
// Handle API requests that fail because Carbon cannot parse the date on validation (when a submitted date value is definitely not a date)
|
||||
if ($e instanceof InvalidFormatException) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('validation.date', ['attribute' => 'date'])), 200);
|
||||
}
|
||||
|
||||
// Handle API requests that fail because the model doesn't exist
|
||||
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
|
||||
$className = last(explode('\\', $e->getModel()));
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $className . ' not found'), 200);
|
||||
}
|
||||
|
||||
// Handle API requests that fail because of an HTTP status code and return a useful error message
|
||||
if ($this->isHttpException($e)) {
|
||||
|
||||
$statusCode = $e->getStatusCode();
|
||||
@@ -122,8 +96,6 @@ class Handler extends ExceptionHandler
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if ($this->isHttpException($e) && (isset($statusCode)) && ($statusCode == '404' )) {
|
||||
return response()->view('layouts/basic', [
|
||||
'content' => view('errors/404')
|
||||
@@ -139,8 +111,8 @@ class Handler extends ExceptionHandler
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Illuminate\Auth\AuthenticationException $exception
|
||||
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
protected function unauthenticated($request, AuthenticationException $exception)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
@@ -150,11 +122,6 @@ class Handler extends ExceptionHandler
|
||||
return redirect()->guest('login');
|
||||
}
|
||||
|
||||
protected function invalidJson($request, ValidationException $exception)
|
||||
{
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $exception->errors()), 200);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A list of the inputs that are never flashed for validation exceptions.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\CustomField;
|
||||
@@ -11,71 +10,12 @@ use App\Models\CustomFieldset;
|
||||
use App\Models\Depreciation;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Statuslabel;
|
||||
use App\Models\License;
|
||||
use Crypt;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Image;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class Helper
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* This is only used for reversing the migration that updates the locale to the 5-6 letter codes from two
|
||||
* letter codes. The normal dropdowns use the autoglossonyms in the language files located
|
||||
* in resources/en-US/localizations.php.
|
||||
*/
|
||||
public static $language_map = [
|
||||
'af' => 'af-ZA', // Afrikaans
|
||||
'am' => 'am-ET', // Amharic
|
||||
'ar' => 'ar-SA', // Arabic
|
||||
'bg' => 'bg-BG', // Bulgarian
|
||||
'ca' => 'ca-ES', // Catalan
|
||||
'cs' => 'cs-CZ', // Czech
|
||||
'cy' => 'cy-GB', // Welsh
|
||||
'da' => 'da-DK', // Danish
|
||||
'de-i' => 'de-if', // German informal
|
||||
'de' => 'de-DE', // German
|
||||
'el' => 'el-GR', // Greek
|
||||
'en' => 'en-US', // English
|
||||
'et' => 'et-EE', // Estonian
|
||||
'fa' => 'fa-IR', // Persian
|
||||
'fi' => 'fi-FI', // Finnish
|
||||
'fil' => 'fil-PH', // Filipino
|
||||
'fr' => 'fr-FR', // French
|
||||
'he' => 'he-IL', // Hebrew
|
||||
'hr' => 'hr-HR', // Croatian
|
||||
'hu' => 'hu-HU', // Hungarian
|
||||
'id' => 'id-ID', // Indonesian
|
||||
'is' => 'is-IS', // Icelandic
|
||||
'it' => 'it-IT', // Italian
|
||||
'iu' => 'iu-NU', // Inuktitut
|
||||
'ja' => 'ja-JP', // Japanese
|
||||
'ko' => 'ko-KR', // Korean
|
||||
'lt' => 'lt-LT', // Lithuanian
|
||||
'lv' => 'lv-LV', // Latvian
|
||||
'mi' => 'mi-NZ', // Maori
|
||||
'mk' => 'mk-MK', // Macedonian
|
||||
'mn' => 'mn-MN', // Mongolian
|
||||
'ms' => 'ms-MY', // Malay
|
||||
'nl' => 'nl-NL', // Dutch
|
||||
'no' => 'no-NO', // Norwegian
|
||||
'pl' => 'pl-PL', // Polish
|
||||
'ro' => 'ro-RO', // Romanian
|
||||
'ru' => 'ru-RU', // Russian
|
||||
'sk' => 'sk-SK', // Slovak
|
||||
'sl' => 'sl-SI', // Slovenian
|
||||
'so' => 'so-SO', // Somali
|
||||
'ta' => 'ta-IN', // Tamil
|
||||
'th' => 'th-TH', // Thai
|
||||
'tl' => 'tl-PH', // Tagalog
|
||||
'tr' => 'tr-TR', // Turkish
|
||||
'uk' => 'uk-UA', // Ukrainian
|
||||
'vi' => 'vi-VN', // Vietnamese
|
||||
'zu' => 'zu-ZA', // Zulu
|
||||
];
|
||||
|
||||
/**
|
||||
* Simple helper to invoke the markdown parser
|
||||
*
|
||||
@@ -83,23 +23,12 @@ class Helper
|
||||
* @since [v2.0]
|
||||
* @return string
|
||||
*/
|
||||
public static function parseEscapedMarkedown($str = null)
|
||||
public static function parseEscapedMarkedown($str)
|
||||
{
|
||||
$Parsedown = new \Parsedown();
|
||||
$Parsedown->setSafeMode(true);
|
||||
|
||||
if ($str) {
|
||||
return $Parsedown->text($str);
|
||||
}
|
||||
}
|
||||
|
||||
public static function parseEscapedMarkedownInline($str = null)
|
||||
{
|
||||
$Parsedown = new \Parsedown();
|
||||
$Parsedown->setSafeMode(true);
|
||||
|
||||
if ($str) {
|
||||
return $Parsedown->line($str);
|
||||
return $Parsedown->text(e($str));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,14 +60,10 @@ class Helper
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v3.3]
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
public static function defaultChartColors(int $index = 0)
|
||||
public static function defaultChartColors($index = 0)
|
||||
{
|
||||
if ($index < 0) {
|
||||
$index = 0;
|
||||
}
|
||||
|
||||
$colors = [
|
||||
'#008941',
|
||||
'#FF4A46',
|
||||
@@ -408,23 +333,7 @@ class Helper
|
||||
'#92896B',
|
||||
];
|
||||
|
||||
$total_colors = count($colors);
|
||||
|
||||
if ($index >= $total_colors) {
|
||||
|
||||
\Log::info('Status label count is '.$index.' and exceeds the allowed count of 266.');
|
||||
//patch fix for array key overflow (color count starts at 1, array starts at 0)
|
||||
$index = $index - $total_colors - 1;
|
||||
|
||||
//constraints to keep result in 0-265 range. This should never be needed, but if something happens
|
||||
//to create this many status labels and it DOES happen, this will keep it from failing at least.
|
||||
if($index < 0) {
|
||||
$index = 0;
|
||||
}
|
||||
elseif($index >($total_colors - 1)) {
|
||||
$index = $total_colors - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return $colors[$index];
|
||||
}
|
||||
@@ -618,23 +527,20 @@ class Helper
|
||||
* @since [v2.5]
|
||||
* @return array
|
||||
*/
|
||||
public static function categoryTypeList($selection=null)
|
||||
public static function categoryTypeList()
|
||||
{
|
||||
$category_types = [
|
||||
'' => '',
|
||||
'accessory' => trans('general.accessory'),
|
||||
'asset' => trans('general.asset'),
|
||||
'consumable' => trans('general.consumable'),
|
||||
'component' => trans('general.component'),
|
||||
'license' => trans('general.license'),
|
||||
'accessory' => 'Accessory',
|
||||
'asset' => 'Asset',
|
||||
'consumable' => 'Consumable',
|
||||
'component' => 'Component',
|
||||
'license' => 'License',
|
||||
];
|
||||
|
||||
if ($selection != null){
|
||||
return $category_types[strtolower($selection)];
|
||||
}
|
||||
else
|
||||
return $category_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of custom fields in an array to make a dropdown menu
|
||||
*
|
||||
@@ -716,19 +622,17 @@ class Helper
|
||||
*/
|
||||
public static function checkLowInventory()
|
||||
{
|
||||
$alert_threshold = \App\Models\Setting::getSettings()->alert_threshold;
|
||||
$consumables = Consumable::withCount('consumableAssignments as consumable_assignments_count')->whereNotNull('min_amt')->get();
|
||||
$accessories = Accessory::withCount('users as users_count')->whereNotNull('min_amt')->get();
|
||||
$components = Component::whereNotNull('min_amt')->get();
|
||||
$asset_models = AssetModel::where('min_amt', '>', 0)->get();
|
||||
$licenses = License::where('min_amt', '>', 0)->get();
|
||||
$components = Component::withCount('assets as assets_count')->whereNotNull('min_amt')->get();
|
||||
|
||||
$avail_consumables = 0;
|
||||
$items_array = [];
|
||||
$all_count = 0;
|
||||
|
||||
foreach ($consumables as $consumable) {
|
||||
$avail = $consumable->numRemaining();
|
||||
if ($avail < ($consumable->min_amt) + $alert_threshold) {
|
||||
if ($avail < ($consumable->min_amt) + \App\Models\Setting::getSettings()->alert_threshold) {
|
||||
if ($consumable->qty > 0) {
|
||||
$percent = number_format((($avail / $consumable->qty) * 100), 0);
|
||||
} else {
|
||||
@@ -747,7 +651,7 @@ class Helper
|
||||
|
||||
foreach ($accessories as $accessory) {
|
||||
$avail = $accessory->qty - $accessory->users_count;
|
||||
if ($avail < ($accessory->min_amt) + $alert_threshold) {
|
||||
if ($avail < ($accessory->min_amt) + \App\Models\Setting::getSettings()->alert_threshold) {
|
||||
if ($accessory->qty > 0) {
|
||||
$percent = number_format((($avail / $accessory->qty) * 100), 0);
|
||||
} else {
|
||||
@@ -765,8 +669,8 @@ class Helper
|
||||
}
|
||||
|
||||
foreach ($components as $component) {
|
||||
$avail = $component->numRemaining();
|
||||
if ($avail < ($component->min_amt) + $alert_threshold) {
|
||||
$avail = $component->qty - $component->assets_count;
|
||||
if ($avail < ($component->min_amt) + \App\Models\Setting::getSettings()->alert_threshold) {
|
||||
if ($component->qty > 0) {
|
||||
$percent = number_format((($avail / $component->qty) * 100), 0);
|
||||
} else {
|
||||
@@ -783,48 +687,6 @@ class Helper
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($asset_models as $asset_model){
|
||||
|
||||
$asset = new Asset();
|
||||
$total_owned = $asset->where('model_id', '=', $asset_model->id)->count();
|
||||
$avail = $asset->where('model_id', '=', $asset_model->id)->whereNull('assigned_to')->count();
|
||||
|
||||
if ($avail < ($asset_model->min_amt) + $alert_threshold) {
|
||||
if ($avail > 0) {
|
||||
$percent = number_format((($avail / $total_owned) * 100), 0);
|
||||
} else {
|
||||
$percent = 100;
|
||||
}
|
||||
$items_array[$all_count]['id'] = $asset_model->id;
|
||||
$items_array[$all_count]['name'] = $asset_model->name;
|
||||
$items_array[$all_count]['type'] = 'models';
|
||||
$items_array[$all_count]['percent'] = $percent;
|
||||
$items_array[$all_count]['remaining'] = $avail;
|
||||
$items_array[$all_count]['min_amt'] = $asset_model->min_amt;
|
||||
$all_count++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($licenses as $license){
|
||||
$avail = $license->remaincount();
|
||||
if ($avail < ($license->min_amt) + $alert_threshold) {
|
||||
if ($avail > 0) {
|
||||
$percent = number_format((($avail / $license->min_amt) * 100), 0);
|
||||
} else {
|
||||
$percent = 100;
|
||||
}
|
||||
|
||||
$items_array[$all_count]['id'] = $license->id;
|
||||
$items_array[$all_count]['name'] = $license->name;
|
||||
$items_array[$all_count]['type'] = 'licenses';
|
||||
$items_array[$all_count]['percent'] = $percent;
|
||||
$items_array[$all_count]['remaining'] = $avail;
|
||||
$items_array[$all_count]['min_amt'] = $license->min_amt;
|
||||
$all_count++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $items_array;
|
||||
}
|
||||
|
||||
@@ -842,7 +704,7 @@ class Helper
|
||||
$filetype = @finfo_file($finfo, $file);
|
||||
finfo_close($finfo);
|
||||
|
||||
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif') || ($filetype == 'image/avif')) {
|
||||
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif')) {
|
||||
return $filetype;
|
||||
}
|
||||
|
||||
@@ -980,16 +842,6 @@ class Helper
|
||||
return preg_replace('/\s+/u', '_', trim($string));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array (or null) of the the raw and formatted date object for easy use in
|
||||
* the API and the bootstrap table listings.
|
||||
*
|
||||
* @param $date
|
||||
* @param $type
|
||||
* @param $array
|
||||
* @return array|string|null
|
||||
*/
|
||||
|
||||
public static function getFormattedDateObject($date, $type = 'datetime', $array = true)
|
||||
{
|
||||
if ($date == '') {
|
||||
@@ -997,42 +849,21 @@ class Helper
|
||||
}
|
||||
|
||||
$settings = Setting::getSettings();
|
||||
$tmp_date = new \Carbon($date);
|
||||
|
||||
/**
|
||||
* Wrap this in a try/catch so that if Carbon crashes, for example if the $date value
|
||||
* isn't actually valid, we don't crash out completely.
|
||||
*
|
||||
* While this *shouldn't* typically happen since we validate dates before entering them
|
||||
* into the database (and we use date/datetime fields for native fields in the system),
|
||||
* it is a possible scenario that a custom field could be created as an "ANY" field, data gets
|
||||
* added, and then the custom field format gets edited later. If someone put bad data in the
|
||||
* database before then - or if they manually edited the field's value - it will crash.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
try {
|
||||
$tmp_date = new \Carbon($date);
|
||||
|
||||
if ($type == 'datetime') {
|
||||
$dt['datetime'] = $tmp_date->format('Y-m-d H:i:s');
|
||||
$dt['formatted'] = $tmp_date->format($settings->date_display_format.' '.$settings->time_display_format);
|
||||
} else {
|
||||
$dt['date'] = $tmp_date->format('Y-m-d');
|
||||
$dt['formatted'] = $tmp_date->format($settings->date_display_format);
|
||||
}
|
||||
|
||||
if ($array == 'true') {
|
||||
return $dt;
|
||||
}
|
||||
|
||||
return $dt['formatted'];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning($e);
|
||||
return $date.' (Invalid '.$type.' value.)';
|
||||
if ($type == 'datetime') {
|
||||
$dt['datetime'] = $tmp_date->format('Y-m-d H:i:s');
|
||||
$dt['formatted'] = $tmp_date->format($settings->date_display_format.' '.$settings->time_display_format);
|
||||
} else {
|
||||
$dt['date'] = $tmp_date->format('Y-m-d');
|
||||
$dt['formatted'] = $tmp_date->format($settings->date_display_format);
|
||||
}
|
||||
|
||||
if ($array == 'true') {
|
||||
return $dt;
|
||||
}
|
||||
|
||||
return $dt['formatted'];
|
||||
}
|
||||
|
||||
// Nicked from Drupal :)
|
||||
@@ -1106,8 +937,6 @@ class Helper
|
||||
'jpeg' => 'far fa-image',
|
||||
'gif' => 'far fa-image',
|
||||
'png' => 'far fa-image',
|
||||
'webp' => 'far fa-image',
|
||||
'avif' => 'far fa-image',
|
||||
// word
|
||||
'doc' => 'far fa-file-word',
|
||||
'docx' => 'far fa-file-word',
|
||||
@@ -1143,8 +972,6 @@ class Helper
|
||||
case 'jpeg':
|
||||
case 'gif':
|
||||
case 'png':
|
||||
case 'webp':
|
||||
case 'avif':
|
||||
return true;
|
||||
break;
|
||||
default:
|
||||
@@ -1232,231 +1059,4 @@ class Helper
|
||||
|
||||
return $file_name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Universal helper to show file size in human-readable formats
|
||||
*
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
* @since 5.0
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function formatFilesizeUnits($bytes)
|
||||
{
|
||||
if ($bytes >= 1073741824)
|
||||
{
|
||||
$bytes = number_format($bytes / 1073741824, 2) . ' GB';
|
||||
}
|
||||
elseif ($bytes >= 1048576)
|
||||
{
|
||||
$bytes = number_format($bytes / 1048576, 2) . ' MB';
|
||||
}
|
||||
elseif ($bytes >= 1024)
|
||||
{
|
||||
$bytes = number_format($bytes / 1024, 2) . ' KB';
|
||||
}
|
||||
elseif ($bytes > 1)
|
||||
{
|
||||
$bytes = $bytes . ' bytes';
|
||||
}
|
||||
elseif ($bytes == 1)
|
||||
{
|
||||
$bytes = $bytes . ' byte';
|
||||
}
|
||||
else
|
||||
{
|
||||
$bytes = '0 bytes';
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is weird but used by the side nav to determine which URL to point the user to
|
||||
*
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
* @since 5.0
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function SettingUrls(){
|
||||
$settings=['#','fields.index', 'statuslabels.index', 'models.index', 'categories.index', 'manufacturers.index', 'suppliers.index', 'departments.index', 'locations.index', 'companies.index', 'depreciations.index'];
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generic helper (largely used by livewire right now) that returns the font-awesome icon
|
||||
* for the object type.
|
||||
*
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
* @since 6.1.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function iconTypeByItem($item) {
|
||||
|
||||
switch ($item) {
|
||||
case 'asset':
|
||||
return 'fas fa-barcode';
|
||||
break;
|
||||
case 'accessory':
|
||||
return 'fas fa-keyboard';
|
||||
break;
|
||||
case 'component':
|
||||
return 'fas fa-hdd';
|
||||
break;
|
||||
case 'consumable':
|
||||
return 'fas fa-tint';
|
||||
break;
|
||||
case 'license':
|
||||
return 'far fa-save';
|
||||
break;
|
||||
case 'location':
|
||||
return 'fas fa-map-marker-alt';
|
||||
break;
|
||||
case 'user':
|
||||
return 'fas fa-user';
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* This is a shorter way to see if the app is in demo mode.
|
||||
*
|
||||
* This makes it cleanly available in blades and in controllers, e.g.
|
||||
*
|
||||
* Blade:
|
||||
* {{ Helper::isDemoMode() ? ' disabled' : ''}} for form blades where we need to disable a form
|
||||
*
|
||||
* Controller:
|
||||
* if (Helper::isDemoMode()) {
|
||||
* // don't allow the thing
|
||||
* }
|
||||
* @todo - use this everywhere else in the app where we have very long if/else config('app.lock_passwords') stuff
|
||||
*/
|
||||
public static function isDemoMode() {
|
||||
if (config('app.lock_passwords') === true) {
|
||||
return true;
|
||||
\Log::debug('app locked!');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Conversion between units of measurement
|
||||
*
|
||||
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
|
||||
* @since 5.0
|
||||
* @param float $value Measurement value to convert
|
||||
* @param string $srcUnit Source unit of measurement
|
||||
* @param string $dstUnit Destination unit of measurement
|
||||
* @param int $round Round the result to decimals (Default false - No rounding)
|
||||
* @return float
|
||||
*/
|
||||
public static function convertUnit($value, $srcUnit, $dstUnit, $round=false) {
|
||||
$srcFactor = static::getUnitConversionFactor($srcUnit);
|
||||
$dstFactor = static::getUnitConversionFactor($dstUnit);
|
||||
$output = $value * $srcFactor / $dstFactor;
|
||||
return ($round !== false) ? round($output, $round) : $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversion factor from unit of measurement to mm
|
||||
*
|
||||
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
|
||||
* @since 5.0
|
||||
* @param string $unit Unit of measurement
|
||||
* @return float
|
||||
*/
|
||||
public static function getUnitConversionFactor($unit) {
|
||||
switch (strtolower($unit)) {
|
||||
case 'mm':
|
||||
return 1.0;
|
||||
case 'cm':
|
||||
return 10.0;
|
||||
case 'm':
|
||||
return 1000.0;
|
||||
case 'in':
|
||||
return 25.4;
|
||||
case 'ft':
|
||||
return 12 * static::getUnitConversionFactor('in');
|
||||
case 'yd':
|
||||
return 3 * static::getUnitConversionFactor('ft');
|
||||
case 'pt':
|
||||
return (1 / 72) * static::getUnitConversionFactor('in');
|
||||
default:
|
||||
throw new \InvalidArgumentException('Unit: \'' . $unit . '\' is not supported');
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* I know it's gauche to return a shitty HTML string, but this is just a helper and since it will be the same every single time,
|
||||
* it seemed pretty safe to do here. Don't you judge me.
|
||||
*/
|
||||
public static function showDemoModeFieldWarning() {
|
||||
if (Helper::isDemoMode()) {
|
||||
return "<p class=\"text-warning\"><i class=\"fas fa-lock\"></i>" . trans('general.feature_disabled') . "</p>";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ah, legacy code.
|
||||
*
|
||||
* This corrects the original mistakes from 2013 where we used the wrong locale codes. Hopefully we
|
||||
* can get rid of this in a future version, but this should at least give us the belt and suspenders we need
|
||||
* to be sure this change is not too disruptive.
|
||||
*
|
||||
* In this array, we ONLY include the older languages where we weren't using the correct locale codes.
|
||||
*
|
||||
* @see public static $language_map in this file
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
* @since 6.3.0
|
||||
*
|
||||
* @param $language_code
|
||||
* @return string []
|
||||
*/
|
||||
public static function mapLegacyLocale($language_code = null)
|
||||
{
|
||||
|
||||
if (strlen($language_code) > 4) {
|
||||
return $language_code;
|
||||
}
|
||||
|
||||
foreach (self::$language_map as $legacy => $new) {
|
||||
if ($language_code == $legacy) {
|
||||
\Log::debug('Current language is '.$legacy.', using '.$new.' instead');
|
||||
return $new;
|
||||
}
|
||||
}
|
||||
|
||||
// Return US english if we don't have a match
|
||||
return 'en-US';
|
||||
}
|
||||
|
||||
public static function mapBackToLegacyLocale($new_locale = null)
|
||||
{
|
||||
if (strlen($new_locale) <= 4) {
|
||||
return $new_locale; //"new locale" apparently wasn't quite so new
|
||||
}
|
||||
|
||||
// This does a *reverse* search against our new language map array - given the value, find the *key* for it
|
||||
$legacy_locale = array_search($new_locale, self::$language_map);
|
||||
|
||||
if($legacy_locale !== false) {
|
||||
return $legacy_locale;
|
||||
}
|
||||
return $new_locale; // better that you have some weird locale that doesn't fit into our mappings anywhere than 'void'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use App\Models\Accessory;
|
||||
use App\Models\Company;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Redirect;
|
||||
|
||||
/** This controller handles all actions related to Accessories for
|
||||
@@ -63,7 +62,6 @@ class AccessoriesController extends Controller
|
||||
public function store(ImageUploadRequest $request)
|
||||
{
|
||||
$this->authorize(Accessory::class);
|
||||
|
||||
// create a new model instance
|
||||
$accessory = new Accessory();
|
||||
|
||||
@@ -77,12 +75,13 @@ class AccessoriesController extends Controller
|
||||
$accessory->manufacturer_id = request('manufacturer_id');
|
||||
$accessory->model_number = request('model_number');
|
||||
$accessory->purchase_date = request('purchase_date');
|
||||
$accessory->purchase_cost = request('purchase_cost');
|
||||
$accessory->purchase_cost = Helper::ParseCurrency(request('purchase_cost'));
|
||||
$accessory->qty = request('qty');
|
||||
$accessory->user_id = Auth::user()->id;
|
||||
$accessory->supplier_id = request('supplier_id');
|
||||
$accessory->notes = request('notes');
|
||||
|
||||
|
||||
$accessory = $request->handleImages($accessory);
|
||||
|
||||
// Was the accessory created?
|
||||
@@ -115,34 +114,6 @@ class AccessoriesController extends Controller
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a view that presents a form to clone an accessory.
|
||||
*
|
||||
* @author [J. Vinsmoke]
|
||||
* @param int $accessoryId
|
||||
* @since [v6.0]
|
||||
* @return View
|
||||
*/
|
||||
public function getClone($accessoryId = null)
|
||||
{
|
||||
|
||||
$this->authorize('create', Accessory::class);
|
||||
|
||||
// Check if the asset exists
|
||||
if (is_null($accessory_to_clone = Accessory::find($accessoryId))) {
|
||||
// Redirect to the asset management page
|
||||
return redirect()->route('accessories.index')
|
||||
->with('error', trans('admin/accessories/message.does_not_exist', ['id' => $accessoryId]));
|
||||
}
|
||||
|
||||
$accessory = clone $accessory_to_clone;
|
||||
$accessory->id = null;
|
||||
$accessory->location_id = null;
|
||||
|
||||
return view('accessories/edit')
|
||||
->with('item', $accessory);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Save edited Accessory from form post
|
||||
@@ -155,47 +126,34 @@ class AccessoriesController extends Controller
|
||||
*/
|
||||
public function update(ImageUploadRequest $request, $accessoryId = null)
|
||||
{
|
||||
if ($accessory = Accessory::withCount('users as users_count')->find($accessoryId)) {
|
||||
|
||||
$this->authorize($accessory);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
"qty" => "required|numeric|min:$accessory->users_count"
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->back()
|
||||
->withErrors($validator)
|
||||
->withInput();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Update the accessory data
|
||||
$accessory->name = request('name');
|
||||
$accessory->location_id = request('location_id');
|
||||
$accessory->min_amt = request('min_amt');
|
||||
$accessory->category_id = request('category_id');
|
||||
$accessory->company_id = Company::getIdForCurrentUser(request('company_id'));
|
||||
$accessory->manufacturer_id = request('manufacturer_id');
|
||||
$accessory->order_number = request('order_number');
|
||||
$accessory->model_number = request('model_number');
|
||||
$accessory->purchase_date = request('purchase_date');
|
||||
$accessory->purchase_cost = request('purchase_cost');
|
||||
$accessory->qty = request('qty');
|
||||
$accessory->supplier_id = request('supplier_id');
|
||||
$accessory->notes = request('notes');
|
||||
|
||||
$accessory = $request->handleImages($accessory);
|
||||
|
||||
// Was the accessory updated?
|
||||
if ($accessory->save()) {
|
||||
return redirect()->route('accessories.index')->with('success', trans('admin/accessories/message.update.success'));
|
||||
}
|
||||
} else {
|
||||
if (is_null($accessory = Accessory::find($accessoryId))) {
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
|
||||
}
|
||||
|
||||
$this->authorize($accessory);
|
||||
|
||||
// Update the accessory data
|
||||
$accessory->name = request('name');
|
||||
$accessory->location_id = request('location_id');
|
||||
$accessory->min_amt = request('min_amt');
|
||||
$accessory->category_id = request('category_id');
|
||||
$accessory->company_id = Company::getIdForCurrentUser(request('company_id'));
|
||||
$accessory->manufacturer_id = request('manufacturer_id');
|
||||
$accessory->order_number = request('order_number');
|
||||
$accessory->model_number = request('model_number');
|
||||
$accessory->purchase_date = request('purchase_date');
|
||||
$accessory->purchase_cost = Helper::ParseCurrency(request('purchase_cost'));
|
||||
$accessory->qty = request('qty');
|
||||
$accessory->supplier_id = request('supplier_id');
|
||||
$accessory->notes = request('notes');
|
||||
|
||||
$accessory = $request->handleImages($accessory);
|
||||
|
||||
// Was the accessory updated?
|
||||
if ($accessory->save()) {
|
||||
return redirect()->route('accessories.index')->with('success', trans('admin/accessories/message.update.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->withErrors($accessory->getErrors());
|
||||
}
|
||||
|
||||
@@ -247,7 +205,7 @@ class AccessoriesController extends Controller
|
||||
*/
|
||||
public function show($accessoryID = null)
|
||||
{
|
||||
$accessory = Accessory::withCount('users as users_count')->find($accessoryID);
|
||||
$accessory = Accessory::find($accessoryID);
|
||||
$this->authorize('view', $accessory);
|
||||
if (isset($accessory->id)) {
|
||||
return view('accessories/view', compact('accessory'));
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Accessories;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Accessory;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Accessory\HttpFoundation\JsonResponse;
|
||||
|
||||
class AccessoriesFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Validates and stores files associated with a accessory.
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
* @param int $accessoryId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*@author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @todo Switch to using the AssetFileRequest form request validator.
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $accessoryId = null)
|
||||
{
|
||||
|
||||
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)->with('success', trans('general.file_upload_success'));
|
||||
|
||||
}
|
||||
|
||||
return redirect()->route('accessories.show', $accessory->id)->with('error', trans('general.no_files_uploaded'));
|
||||
}
|
||||
// Prepare the error message
|
||||
return redirect()->route('accessories.index')
|
||||
->with('error', trans('general.file_does_not_exist'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the selected accessory file.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @param int $accessoryId
|
||||
* @param int $fileId
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function destroy($accessoryId = null, $fileId = null)
|
||||
{
|
||||
$accessory = Accessory::find($accessoryId);
|
||||
|
||||
// the asset is valid
|
||||
if (isset($accessory->id)) {
|
||||
$this->authorize('update', $accessory);
|
||||
$log = Actionlog::find($fileId);
|
||||
|
||||
// Remove the file if one exists
|
||||
if (Storage::exists('accessories/'.$log->filename)) {
|
||||
try {
|
||||
Storage::delete('accessories/'.$log->filename);
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug($e);
|
||||
}
|
||||
}
|
||||
|
||||
$log->delete();
|
||||
|
||||
return redirect()->back()
|
||||
->with('success', trans('admin/hardware/message.deletefile.success'));
|
||||
}
|
||||
|
||||
// Redirect to the licence management page
|
||||
return redirect()->route('accessories.index')->with('error', trans('general.file_does_not_exist'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the selected file to be viewed.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.4]
|
||||
* @param int $accessoryId
|
||||
* @param int $fileId
|
||||
* @return \Symfony\Accessory\HttpFoundation\Response
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function show($accessoryId = null, $fileId = null, $download = true)
|
||||
{
|
||||
|
||||
\Log::debug('Private filesystem is: '.config('filesystems.default'));
|
||||
$accessory = Accessory::find($accessoryId);
|
||||
|
||||
|
||||
|
||||
// the accessory is valid
|
||||
if (isset($accessory->id)) {
|
||||
$this->authorize('view', $accessory);
|
||||
$this->authorize('accessories.files', $accessory);
|
||||
|
||||
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $accessory->id)->find($fileId)) {
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/users/message.log_record_not_found'));
|
||||
}
|
||||
|
||||
$file = 'private_uploads/accessories/'.$log->filename;
|
||||
|
||||
if (Storage::missing($file)) {
|
||||
\Log::debug('FILE DOES NOT EXISTS for '.$file);
|
||||
\Log::debug('URL should be '.Storage::url($file));
|
||||
|
||||
return response('File '.$file.' ('.Storage::url($file).') not found on server', 404)
|
||||
->header('Content-Type', 'text/plain');
|
||||
} else {
|
||||
|
||||
// Display the file inline
|
||||
if (request('inline') == 'true') {
|
||||
$headers = [
|
||||
'Content-Disposition' => 'inline',
|
||||
];
|
||||
return Storage::download($file, $log->filename, $headers);
|
||||
}
|
||||
|
||||
|
||||
// We have to override the URL stuff here, since local defaults in Laravel's Flysystem
|
||||
// won't work, as they're not accessible via the web
|
||||
if (config('filesystems.default') == 'local') { // TODO - is there any way to fix this at the StorageHelper layer?
|
||||
return StorageHelper::downloader($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('accessories.index')->with('error', trans('general.file_does_not_exist', ['id' => $fileId]));
|
||||
}
|
||||
}
|
||||
@@ -60,10 +60,9 @@ class AccessoryCheckinController extends Controller
|
||||
|
||||
$this->authorize('checkin', $accessory);
|
||||
|
||||
$checkin_hours = date('H:i:s');
|
||||
$checkin_at = date('Y-m-d H:i:s');
|
||||
$checkin_at = date('Y-m-d');
|
||||
if ($request->filled('checkin_at')) {
|
||||
$checkin_at = $request->input('checkin_at').' '.$checkin_hours;
|
||||
$checkin_at = $request->input('checkin_at');
|
||||
}
|
||||
|
||||
// Was the accessory updated?
|
||||
|
||||
@@ -18,36 +18,26 @@ class AccessoryCheckoutController extends Controller
|
||||
* Return the form to checkout an Accessory to a user.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $id
|
||||
* @param int $accessoryId
|
||||
* @return View
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function create($id)
|
||||
public function create($accessoryId)
|
||||
{
|
||||
|
||||
if ($accessory = Accessory::withCount('users as users_count')->find($id)) {
|
||||
|
||||
$this->authorize('checkout', $accessory);
|
||||
|
||||
if ($accessory->category) {
|
||||
// Make sure there is at least one available to checkout
|
||||
if ($accessory->numRemaining() <= 0){
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkout.unavailable'));
|
||||
}
|
||||
|
||||
// Return the checkout view
|
||||
return view('accessories/checkout', compact('accessory'));
|
||||
}
|
||||
|
||||
// Invalid category
|
||||
return redirect()->route('accessories.edit', ['accessory' => $accessory->id])
|
||||
->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.accessory')]));
|
||||
|
||||
// Check if the accessory exists
|
||||
if (is_null($accessory = Accessory::find($accessoryId))) {
|
||||
// Redirect to the accessory management page with error
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
|
||||
}
|
||||
|
||||
// Not found
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
|
||||
if ($accessory->category) {
|
||||
$this->authorize('checkout', $accessory);
|
||||
|
||||
// Get the dropdown of users and then pass it to the checkout view
|
||||
return view('accessories/checkout', compact('accessory'));
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'The category type for this accessory is not valid. Edit the accessory and select a valid accessory category.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,23 +55,17 @@ class AccessoryCheckoutController extends Controller
|
||||
public function store(Request $request, $accessoryId)
|
||||
{
|
||||
// Check if the accessory exists
|
||||
if (is_null($accessory = Accessory::withCount('users as users_count')->find($accessoryId))) {
|
||||
if (is_null($accessory = Accessory::find($accessoryId))) {
|
||||
// Redirect to the accessory management page with error
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.user_not_found'));
|
||||
}
|
||||
|
||||
$this->authorize('checkout', $accessory);
|
||||
|
||||
if (!$user = User::find($request->input('assigned_to'))) {
|
||||
return redirect()->route('accessories.checkout.show', $accessory->id)->with('error', trans('admin/accessories/message.checkout.user_does_not_exist'));
|
||||
if (! $user = User::find($request->input('assigned_to'))) {
|
||||
return redirect()->route('checkout/accessory', $accessory->id)->with('error', trans('admin/accessories/message.checkout.user_does_not_exist'));
|
||||
}
|
||||
|
||||
// Make sure there is at least one available to checkout
|
||||
if ($accessory->numRemaining() <= 0){
|
||||
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkout.unavailable'));
|
||||
}
|
||||
|
||||
|
||||
// Update the accessory data
|
||||
$accessory->assigned_to = e($request->input('assigned_to'));
|
||||
|
||||
|
||||
@@ -7,29 +7,13 @@ use App\Events\CheckoutDeclined;
|
||||
use App\Events\ItemAccepted;
|
||||
use App\Events\ItemDeclined;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\Company;
|
||||
use App\Models\Contracts\Acceptable;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\License;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Notifications\AcceptanceAssetAcceptedNotification;
|
||||
use App\Notifications\AcceptanceAssetDeclinedNotification;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Http\Controllers\SettingsController;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Carbon\Carbon;
|
||||
use phpDocumentor\Reflection\Types\Compound;
|
||||
|
||||
class AcceptanceController extends Controller
|
||||
{
|
||||
@@ -55,7 +39,6 @@ class AcceptanceController extends Controller
|
||||
{
|
||||
$acceptance = CheckoutAcceptance::find($id);
|
||||
|
||||
|
||||
if (is_null($acceptance)) {
|
||||
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||
}
|
||||
@@ -69,7 +52,7 @@ class AcceptanceController extends Controller
|
||||
}
|
||||
|
||||
if (! Company::isCurrentUserHasAccess($acceptance->checkoutable)) {
|
||||
return redirect()->route('account.accept')->with('error', trans('general.error_user_company'));
|
||||
return redirect()->route('account.accept')->with('error', trans('general.insufficient_permissions'));
|
||||
}
|
||||
|
||||
return view('account/accept.create', compact('acceptance'));
|
||||
@@ -113,224 +96,29 @@ class AcceptanceController extends Controller
|
||||
Storage::makeDirectory('private_uploads/signatures', 775);
|
||||
}
|
||||
|
||||
|
||||
|
||||
$item = $acceptance->checkoutable_type::find($acceptance->checkoutable_id);
|
||||
$display_model = '';
|
||||
$pdf_view_route = '';
|
||||
$pdf_filename = 'accepted-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
||||
$sig_filename='';
|
||||
$sig_filename = '';
|
||||
if ($request->filled('signature_output')) {
|
||||
$sig_filename = 'siglog-'.Str::uuid().'-'.date('Y-m-d-his').'.png';
|
||||
$data_uri = e($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);
|
||||
}
|
||||
|
||||
if ($request->input('asset_acceptance') == 'accepted') {
|
||||
$acceptance->accept($sig_filename);
|
||||
|
||||
/**
|
||||
* 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'));
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
'eula' => $item->getEula(),
|
||||
'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);
|
||||
$acceptance->notify(new AcceptanceAssetAcceptedNotification($data));
|
||||
event(new CheckoutAccepted($acceptance));
|
||||
|
||||
$return_msg = trans('admin/users/message.accepted');
|
||||
|
||||
} else {
|
||||
|
||||
/**
|
||||
* 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,
|
||||
'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);
|
||||
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
|
||||
|
||||
event(new CheckoutDeclined($acceptance));
|
||||
|
||||
$return_msg = trans('admin/users/message.declined');
|
||||
}
|
||||
|
||||
|
||||
return redirect()->to('account/accept')->with('success', $return_msg);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Models\Actionlog;
|
||||
use Response;
|
||||
|
||||
class ActionlogController extends Controller
|
||||
@@ -27,10 +26,4 @@ class ActionlogController extends Controller
|
||||
}
|
||||
|
||||
}
|
||||
public function getStoredEula($filename){
|
||||
$this->authorize('view', \App\Models\Asset::class);
|
||||
$file = config('app.private_uploads').'/eula-pdfs/'.$filename;
|
||||
|
||||
return Response::download($file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\AccessoriesTransformer;
|
||||
@@ -27,11 +26,9 @@ class AccessoriesController extends Controller
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
if ($request->user()->cannot('reports.view')) {
|
||||
$this->authorize('view', Accessory::class);
|
||||
}
|
||||
|
||||
|
||||
$this->authorize('view', Accessory::class);
|
||||
$allowed_columns = ['id', 'name', 'model_number', 'eol', 'notes', 'created_at', 'min_amt', 'company_id'];
|
||||
|
||||
// This array is what determines which fields should be allowed to be sorted on ON the table itself, no relations
|
||||
// Relations will be handled in query scopes a little further down.
|
||||
$allowed_columns =
|
||||
@@ -45,13 +42,10 @@ class AccessoriesController extends Controller
|
||||
'min_amt',
|
||||
'company_id',
|
||||
'notes',
|
||||
'users_count',
|
||||
'qty',
|
||||
];
|
||||
|
||||
|
||||
$accessories = Accessory::select('accessories.*')->with('category', 'company', 'manufacturer', 'users', 'location', 'supplier')
|
||||
->withCount('users as users_count');
|
||||
$accessories = Accessory::select('accessories.*')->with('category', 'company', 'manufacturer', 'users', 'location', 'supplier');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$accessories = $accessories->TextSearch($request->input('search'));
|
||||
@@ -81,9 +75,12 @@ class AccessoriesController extends Controller
|
||||
$accessories->where('notes','=',$request->input('notes'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $accessories->count()) ? $accessories->count() : abs($request->input('offset'));
|
||||
$limit = app('api_limit_value');
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($accessories) && ($request->get('offset') > $accessories->count())) ? $accessories->count() : $request->get('offset', 0);
|
||||
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort_override = $request->input('sort');
|
||||
@@ -151,7 +148,7 @@ class AccessoriesController extends Controller
|
||||
public function show($id)
|
||||
{
|
||||
$this->authorize('view', Accessory::class);
|
||||
$accessory = Accessory::withCount('users as users_count')->findOrFail($id);
|
||||
$accessory = Accessory::findOrFail($id);
|
||||
|
||||
return (new AccessoriesTransformer)->transformAccessory($accessory);
|
||||
}
|
||||
@@ -279,7 +276,7 @@ class AccessoriesController extends Controller
|
||||
public function checkout(Request $request, $accessoryId)
|
||||
{
|
||||
// Check if the accessory exists
|
||||
if (is_null($accessory = Accessory::withCount('users as users_count')->find($accessoryId))) {
|
||||
if (is_null($accessory = Accessory::find($accessoryId))) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.does_not_exist')));
|
||||
}
|
||||
|
||||
@@ -303,7 +300,7 @@ class AccessoriesController extends Controller
|
||||
'note' => $request->get('note'),
|
||||
]);
|
||||
|
||||
event(new CheckoutableCheckedOut($accessory, $user, Auth::user(), $request->input('note')));
|
||||
$accessory->logCheckout($request->input('note'), $user);
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.checkout.success')));
|
||||
}
|
||||
@@ -332,7 +329,7 @@ class AccessoriesController extends Controller
|
||||
$accessory = Accessory::find($accessory_user->accessory_id);
|
||||
$this->authorize('checkin', $accessory);
|
||||
|
||||
$logaction = $accessory->logCheckin(User::find($accessory_user->assigned_to), $request->input('note'));
|
||||
$logaction = $accessory->logCheckin(User::find($accessory_user->user_id), $request->input('note'));
|
||||
|
||||
// Was the accessory updated?
|
||||
if (DB::table('accessories_users')->where('id', '=', $accessory_user->id)->delete()) {
|
||||
|
||||
@@ -35,9 +35,7 @@ class AssetMaintenancesController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('view', Asset::class);
|
||||
|
||||
$maintenances = AssetMaintenance::select('asset_maintenances.*')
|
||||
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'admin');
|
||||
$maintenances = AssetMaintenance::with('asset', 'asset.model', 'asset.location', 'supplier', 'asset.company', 'admin');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$maintenances = $maintenances->TextSearch($request->input('search'));
|
||||
@@ -47,18 +45,12 @@ class AssetMaintenancesController extends Controller
|
||||
$maintenances->where('asset_id', '=', $request->input('asset_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
|
||||
}
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($maintenances) && ($request->get('offset') > $maintenances->count())) ? $maintenances->count() : $request->get('offset', 0);
|
||||
|
||||
if ($request->filled('asset_maintenance_type')) {
|
||||
$maintenances->where('asset_maintenance_type', '=', $request->input('asset_maintenance_type'));
|
||||
}
|
||||
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $maintenances->count()) ? $maintenances->count() : abs($request->input('offset'));
|
||||
$limit = app('api_limit_value');
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$allowed_columns = [
|
||||
'id',
|
||||
@@ -71,13 +63,8 @@ class AssetMaintenancesController extends Controller
|
||||
'notes',
|
||||
'asset_tag',
|
||||
'asset_name',
|
||||
'serial',
|
||||
'user_id',
|
||||
'supplier',
|
||||
'is_warranty',
|
||||
'status_label',
|
||||
];
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
|
||||
|
||||
@@ -85,21 +72,12 @@ class AssetMaintenancesController extends Controller
|
||||
case 'user_id':
|
||||
$maintenances = $maintenances->OrderAdmin($order);
|
||||
break;
|
||||
case 'supplier':
|
||||
$maintenances = $maintenances->OrderBySupplier($order);
|
||||
break;
|
||||
case 'asset_tag':
|
||||
$maintenances = $maintenances->OrderByTag($order);
|
||||
break;
|
||||
case 'asset_name':
|
||||
$maintenances = $maintenances->OrderByAssetName($order);
|
||||
break;
|
||||
case 'serial':
|
||||
$maintenances = $maintenances->OrderByAssetSerial($order);
|
||||
break;
|
||||
case 'status_label':
|
||||
$maintenances = $maintenances->OrderStatusName($order);
|
||||
break;
|
||||
default:
|
||||
$maintenances = $maintenances->orderBy($sort, $order);
|
||||
break;
|
||||
@@ -126,17 +104,41 @@ class AssetMaintenancesController extends Controller
|
||||
{
|
||||
$this->authorize('update', Asset::class);
|
||||
// create a new model instance
|
||||
$maintenance = new AssetMaintenance();
|
||||
$maintenance->fill($request->all());
|
||||
$maintenance->user_id = Auth::id();
|
||||
$assetMaintenance = new AssetMaintenance();
|
||||
$assetMaintenance->supplier_id = $request->input('supplier_id');
|
||||
$assetMaintenance->is_warranty = $request->input('is_warranty');
|
||||
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
|
||||
$assetMaintenance->notes = e($request->input('notes'));
|
||||
$asset = Asset::find(e($request->input('asset_id')));
|
||||
|
||||
if (! Company::isCurrentUserHasAccess($asset)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot add a maintenance for that asset'));
|
||||
}
|
||||
|
||||
// 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->user_id = 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 ($maintenance->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.create.success')));
|
||||
if ($assetMaintenance->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.create.success')));
|
||||
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $assetMaintenance->getErrors()));
|
||||
|
||||
}
|
||||
|
||||
@@ -144,39 +146,65 @@ class AssetMaintenancesController extends Controller
|
||||
* Validates and stores an update to an asset maintenance
|
||||
*
|
||||
* @author A. Gianotto <snipe@snipe.net>
|
||||
* @param int $id
|
||||
* @param int $assetMaintenanceId
|
||||
* @param int $request
|
||||
* @version v1.0
|
||||
* @since [v4.0]
|
||||
* @return string JSON
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
public function update(Request $request, $assetMaintenanceId = null)
|
||||
{
|
||||
$this->authorize('update', Asset::class);
|
||||
// Check if the asset maintenance exists
|
||||
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
|
||||
|
||||
if ($maintenance = AssetMaintenance::with('asset')->find($id)) {
|
||||
|
||||
// Can this user manage this asset?
|
||||
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
|
||||
}
|
||||
|
||||
// The asset this miantenance is attached to is not valid or has been deleted
|
||||
if (!$maintenance->asset) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('general.asset'), 'id' => $id])));
|
||||
}
|
||||
|
||||
$maintenance->fill($request->all());
|
||||
|
||||
if ($maintenance->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.edit.success')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
|
||||
if (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot edit a maintenance for that asset'));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id])));
|
||||
$assetMaintenance->supplier_id = e($request->input('supplier_id'));
|
||||
$assetMaintenance->is_warranty = e($request->input('is_warranty'));
|
||||
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
|
||||
$assetMaintenance->notes = e($request->input('notes'));
|
||||
|
||||
$asset = Asset::find(request('asset_id'));
|
||||
|
||||
if (! Company::isCurrentUserHasAccess($asset)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot edit a maintenance for that asset'));
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
if (($assetMaintenance->completion_date == null)
|
||||
) {
|
||||
if (($assetMaintenance->asset_maintenance_time !== 0)
|
||||
|| (! is_null($assetMaintenance->asset_maintenance_time))
|
||||
) {
|
||||
$assetMaintenance->asset_maintenance_time = null;
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.edit.success')));
|
||||
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $assetMaintenance->getErrors()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,6 @@ class AssetModelsController extends Controller
|
||||
'image',
|
||||
'name',
|
||||
'model_number',
|
||||
'min_amt',
|
||||
'eol',
|
||||
'notes',
|
||||
'created_at',
|
||||
@@ -46,7 +45,6 @@ class AssetModelsController extends Controller
|
||||
'requestable',
|
||||
'assets_count',
|
||||
'category',
|
||||
'fieldset',
|
||||
];
|
||||
|
||||
$assetmodels = AssetModel::select([
|
||||
@@ -54,7 +52,6 @@ class AssetModelsController extends Controller
|
||||
'models.image',
|
||||
'models.name',
|
||||
'model_number',
|
||||
'min_amt',
|
||||
'eol',
|
||||
'requestable',
|
||||
'models.notes',
|
||||
@@ -66,24 +63,23 @@ class AssetModelsController extends Controller
|
||||
'models.deleted_at',
|
||||
'models.updated_at',
|
||||
])
|
||||
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues')
|
||||
->with('category', 'depreciation', 'manufacturer', 'fieldset')
|
||||
->withCount('assets as assets_count');
|
||||
|
||||
if ($request->input('status')=='deleted') {
|
||||
$assetmodels->onlyTrashed();
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$assetmodels = $assetmodels->where('models.category_id', '=', $request->input('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$assetmodels->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $assetmodels->count()) ? $assetmodels->count() : abs($request->input('offset'));
|
||||
$limit = app('api_limit_value');
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($assetmodels) && ($request->get('offset') > $assetmodels->count())) ? $assetmodels->count() : $request->get('offset', 0);
|
||||
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'models.created_at';
|
||||
@@ -95,9 +91,6 @@ class AssetModelsController extends Controller
|
||||
case 'category':
|
||||
$assetmodels->OrderCategory($order);
|
||||
break;
|
||||
case 'fieldset':
|
||||
$assetmodels->OrderFieldset($order);
|
||||
break;
|
||||
default:
|
||||
$assetmodels->orderBy($sort, $order);
|
||||
break;
|
||||
|
||||
@@ -3,20 +3,15 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Http\Requests\StoreAssetRequest;
|
||||
use App\Http\Traits\MigratesLegacyAssetLocations;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\LicenseSeat;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AssetCheckoutRequest;
|
||||
use App\Http\Transformers\AssetsTransformer;
|
||||
use App\Http\Transformers\DepreciationReportTransformer;
|
||||
use App\Http\Transformers\LicensesTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Company;
|
||||
@@ -25,12 +20,11 @@ use App\Models\License;
|
||||
use App\Models\Location;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use \Illuminate\Support\Facades\Auth;
|
||||
use Auth;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Input;
|
||||
use Paginator;
|
||||
use Slack;
|
||||
@@ -39,7 +33,6 @@ use TCPDF;
|
||||
use Validator;
|
||||
use Route;
|
||||
|
||||
|
||||
/**
|
||||
* This class controls all actions related to assets for
|
||||
* the Snipe-IT Asset Management application.
|
||||
@@ -49,19 +42,17 @@ use Route;
|
||||
*/
|
||||
class AssetsController extends Controller
|
||||
{
|
||||
use MigratesLegacyAssetLocations;
|
||||
|
||||
/**
|
||||
* Returns JSON listing of all assets
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $assetId
|
||||
* @since [v4.0]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function index(Request $request, $audit = null)
|
||||
{
|
||||
|
||||
\Log::debug(Route::currentRouteName());
|
||||
$filter_non_deprecable_assets = false;
|
||||
|
||||
/**
|
||||
@@ -94,7 +85,6 @@ class AssetsController extends Controller
|
||||
'serial',
|
||||
'model_number',
|
||||
'last_checkout',
|
||||
'last_checkin',
|
||||
'notes',
|
||||
'expected_checkin',
|
||||
'order_number',
|
||||
@@ -110,9 +100,6 @@ class AssetsController extends Controller
|
||||
'checkout_counter',
|
||||
'checkin_counter',
|
||||
'requests_counter',
|
||||
'byod',
|
||||
'asset_eol_date',
|
||||
'requestable',
|
||||
];
|
||||
|
||||
$filter = [];
|
||||
@@ -126,35 +113,87 @@ class AssetsController extends Controller
|
||||
$allowed_columns[] = $field->db_column_name();
|
||||
}
|
||||
|
||||
$assets = Asset::select('assets.*')
|
||||
$assets = Company::scopeCompanyables(Asset::select('assets.*'), 'company_id', 'assets')
|
||||
->with('location', 'assetstatus', 'company', 'defaultLoc','assignedTo',
|
||||
'model.category', 'model.manufacturer', 'model.fieldset','supplier'); //it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
|
||||
|
||||
|
||||
if ($filter_non_deprecable_assets) {
|
||||
$non_deprecable_models = AssetModel::select('id')->whereNotNull('depreciation_id')->get();
|
||||
|
||||
$assets->InModelList($non_deprecable_models->toArray());
|
||||
}
|
||||
|
||||
|
||||
|
||||
// These are used by the API to query against specific ID numbers.
|
||||
// They are also used by the individual searches on detail pages like
|
||||
// locations, etc.
|
||||
|
||||
|
||||
// Search custom fields by column name
|
||||
foreach ($all_custom_fields as $field) {
|
||||
if ($request->filled($field->db_column_name()) && $field->db_column_name()) {
|
||||
if ($request->filled($field->db_column_name())) {
|
||||
$assets->where($field->db_column_name(), '=', $request->input($field->db_column_name()));
|
||||
}
|
||||
}
|
||||
|
||||
if ((! is_null($filter)) && (count($filter)) > 0) {
|
||||
$assets->ByFilter($filter);
|
||||
} elseif ($request->filled('search')) {
|
||||
$assets->TextSearch($request->input('search'));
|
||||
|
||||
if ($request->filled('status_id')) {
|
||||
$assets->where('assets.status_id', '=', $request->input('status_id'));
|
||||
}
|
||||
|
||||
if ($request->input('requestable') == 'true') {
|
||||
$assets->where('assets.requestable', '=', '1');
|
||||
}
|
||||
|
||||
if ($request->filled('model_id')) {
|
||||
$assets->InModelList([$request->input('model_id')]);
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$assets->InCategory($request->input('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$assets->where('assets.location_id', '=', $request->input('location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('rtd_location_id')) {
|
||||
$assets->where('assets.rtd_location_id', '=', $request->input('rtd_location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$assets->where('assets.supplier_id', '=', $request->input('supplier_id'));
|
||||
}
|
||||
|
||||
if (($request->filled('assigned_to')) && ($request->filled('assigned_type'))) {
|
||||
$assets->where('assets.assigned_to', '=', $request->input('assigned_to'))
|
||||
->where('assets.assigned_type', '=', $request->input('assigned_type'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$assets->where('assets.company_id', '=', $request->input('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$assets->ByManufacturer($request->input('manufacturer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('depreciation_id')) {
|
||||
$assets->ByDepreciationId($request->input('depreciation_id'));
|
||||
}
|
||||
|
||||
$request->filled('order_number') ? $assets = $assets->where('assets.order_number', '=', e($request->get('order_number'))) : '';
|
||||
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($assets) && ($request->get('offset') > $assets->count())) ? $assets->count() : $request->get('offset', 0);
|
||||
|
||||
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
// This is used by the audit reporting routes
|
||||
if (Gate::allows('audit', Asset::class)) {
|
||||
switch ($audit) {
|
||||
@@ -168,6 +207,7 @@ class AssetsController extends Controller
|
||||
}
|
||||
|
||||
|
||||
|
||||
// This is used by the sidenav, mostly
|
||||
|
||||
// We switched from using query scopes here because of a Laravel bug
|
||||
@@ -217,12 +257,7 @@ class AssetsController extends Controller
|
||||
break;
|
||||
case 'Deployed':
|
||||
// more sad, horrible workarounds for laravel bugs when doing full text searches
|
||||
$assets->whereNotNull('assets.assigned_to');
|
||||
break;
|
||||
case 'byod':
|
||||
// This is kind of redundant, since we already check for byod=1 above, but this keeps the
|
||||
// sidebar nav links a little less chaotic
|
||||
$assets->where('assets.byod', '=', '1');
|
||||
$assets->where('assets.assigned_to', '>', '0');
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -243,71 +278,12 @@ class AssetsController extends Controller
|
||||
}
|
||||
|
||||
|
||||
// Leave these under the TextSearch scope, else the fuzziness will override the specific ID (status ID, etc) requested
|
||||
if ($request->filled('status_id')) {
|
||||
$assets->where('assets.status_id', '=', $request->input('status_id'));
|
||||
if ((! is_null($filter)) && (count($filter)) > 0) {
|
||||
$assets->ByFilter($filter);
|
||||
} elseif ($request->filled('search')) {
|
||||
$assets->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('asset_tag')) {
|
||||
$assets->where('assets.asset_tag', '=', $request->input('asset_tag'));
|
||||
}
|
||||
|
||||
if ($request->filled('serial')) {
|
||||
$assets->where('assets.serial', '=', $request->input('serial'));
|
||||
}
|
||||
|
||||
if ($request->input('requestable') == 'true') {
|
||||
$assets->where('assets.requestable', '=', '1');
|
||||
}
|
||||
|
||||
if ($request->filled('model_id')) {
|
||||
$assets->InModelList([$request->input('model_id')]);
|
||||
}
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$assets->InCategory($request->input('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$assets->where('assets.location_id', '=', $request->input('location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('rtd_location_id')) {
|
||||
$assets->where('assets.rtd_location_id', '=', $request->input('rtd_location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$assets->where('assets.supplier_id', '=', $request->input('supplier_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('asset_eol_date')) {
|
||||
$assets->where('assets.asset_eol_date', '=', $request->input('asset_eol_date'));
|
||||
}
|
||||
|
||||
if (($request->filled('assigned_to')) && ($request->filled('assigned_type'))) {
|
||||
$assets->where('assets.assigned_to', '=', $request->input('assigned_to'))
|
||||
->where('assets.assigned_type', '=', $request->input('assigned_type'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$assets->where('assets.company_id', '=', $request->input('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manufacturer_id')) {
|
||||
$assets->ByManufacturer($request->input('manufacturer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('depreciation_id')) {
|
||||
$assets->ByDepreciationId($request->input('depreciation_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('byod')) {
|
||||
$assets->where('assets.byod', '=', $request->input('byod'));
|
||||
}
|
||||
|
||||
if ($request->filled('order_number')) {
|
||||
$assets->where('assets.order_number', '=', strval($request->get('order_number')));
|
||||
}
|
||||
|
||||
// This is kinda gross, but we need to do this because the Bootstrap Tables
|
||||
// API passes custom field ordering as custom_fields.fieldname, and we have to strip
|
||||
@@ -318,8 +294,7 @@ class AssetsController extends Controller
|
||||
// in the allowed_columns array)
|
||||
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.created_at';
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
|
||||
switch ($sort_override) {
|
||||
case 'model':
|
||||
$assets->OrderModels($order);
|
||||
@@ -356,10 +331,6 @@ class AssetsController extends Controller
|
||||
}
|
||||
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $assets->count()) ? $assets->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
|
||||
$total = $assets->count();
|
||||
$assets = $assets->skip($offset)->take($limit)->get();
|
||||
|
||||
@@ -374,7 +345,6 @@ 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.
|
||||
@@ -386,38 +356,19 @@ class AssetsController extends Controller
|
||||
/**
|
||||
* Returns JSON with information about an asset (by tag) for detail view.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param string $tag
|
||||
* @since [v4.2.1]
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function showByTag(Request $request, $tag)
|
||||
{
|
||||
$this->authorize('index', Asset::class);
|
||||
$assets = Asset::where('asset_tag', $tag)->with('assetstatus')->with('assignedTo');
|
||||
if ($asset = Asset::with('assetstatus')->with('assignedTo')->where('asset_tag', $tag)->first()) {
|
||||
$this->authorize('view', $asset);
|
||||
|
||||
// Check if they've passed ?deleted=true
|
||||
if ($request->input('deleted', 'false') == 'true') {
|
||||
$assets = $assets->withTrashed();
|
||||
return (new AssetsTransformer)->transformAsset($asset, $request);
|
||||
}
|
||||
|
||||
if (($assets = $assets->get()) && ($assets->count()) > 0) {
|
||||
|
||||
// If there is exactly one result and the deleted parameter is not passed, we should pull the first (and only)
|
||||
// asset from the returned collection, since transformAsset() expects an Asset object, NOT a collection
|
||||
if (($assets->count() == 1) && ($request->input('deleted') != 'true')) {
|
||||
return (new AssetsTransformer)->transformAsset($assets->first());
|
||||
|
||||
// If there is more than one result OR if the endpoint is requesting deleted items (even if there is only one
|
||||
// match, return the normal collection transformed.
|
||||
} else {
|
||||
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// If there are 0 results, return the "no such asset" response
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Asset not found'), 200);
|
||||
|
||||
}
|
||||
|
||||
@@ -427,25 +378,29 @@ class AssetsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param string $serial
|
||||
* @since [v4.2.1]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function showBySerial(Request $request, $serial)
|
||||
{
|
||||
$this->authorize('index', Asset::class);
|
||||
$assets = Asset::where('serial', $serial)->with('assetstatus')->with('assignedTo');
|
||||
if ($assets = Asset::with('assetstatus')->with('assignedTo')
|
||||
->withTrashed()->where('serial', $serial)->get()) {
|
||||
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
|
||||
}
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Asset not found'), 200);
|
||||
|
||||
// Check if they've passed ?deleted=true
|
||||
if ($request->input('deleted', 'false') == 'true') {
|
||||
$assets = Asset::with('assetstatus')->with('assignedTo');
|
||||
|
||||
if ($request->input('deleted', 'false') === 'true') {
|
||||
$assets = $assets->withTrashed();
|
||||
}
|
||||
|
||||
if (($assets = $assets->get()) && ($assets->count()) > 0) {
|
||||
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
|
||||
}
|
||||
|
||||
// If there are 0 results, return the "no such asset" response
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
|
||||
}
|
||||
|
||||
$assets = $assets->where('serial', $serial)->get();
|
||||
if ($assets) {
|
||||
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
|
||||
} else {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Asset not found'), 200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -454,7 +409,7 @@ class AssetsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $assetId
|
||||
* @since [v4.0]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function show(Request $request, $id)
|
||||
{
|
||||
@@ -472,7 +427,7 @@ class AssetsController extends Controller
|
||||
{
|
||||
$this->authorize('view', Asset::class);
|
||||
$this->authorize('view', License::class);
|
||||
$asset = Asset::where('id', $id)->withTrashed()->firstorfail();
|
||||
$asset = Asset::where('id', $id)->withTrashed()->first();
|
||||
$licenses = $asset->licenses()->get();
|
||||
|
||||
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
|
||||
@@ -485,12 +440,12 @@ class AssetsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v4.0.16]
|
||||
* @see \App\Http\Transformers\SelectlistTransformer
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*
|
||||
*/
|
||||
public function selectlist(Request $request)
|
||||
{
|
||||
|
||||
$assets = Asset::select([
|
||||
$assets = Company::scopeCompanyables(Asset::select([
|
||||
'assets.id',
|
||||
'assets.name',
|
||||
'assets.asset_tag',
|
||||
@@ -498,7 +453,7 @@ class AssetsController extends Controller
|
||||
'assets.assigned_to',
|
||||
'assets.assigned_type',
|
||||
'assets.status_id',
|
||||
])->with('model', 'assetstatus', 'assignedTo')->NotArchived();
|
||||
])->with('model', 'assetstatus', 'assignedTo')->NotArchived(), 'company_id', 'assets');
|
||||
|
||||
if ($request->filled('assetStatusType') && $request->input('assetStatusType') === 'RTD') {
|
||||
$assets = $assets->RTD();
|
||||
@@ -541,14 +496,35 @@ class AssetsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param \App\Http\Requests\ImageUploadRequest $request
|
||||
* @since [v4.0]
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function store(StoreAssetRequest $request): JsonResponse
|
||||
public function store(ImageUploadRequest $request)
|
||||
{
|
||||
$this->authorize('create', Asset::class);
|
||||
|
||||
$asset = new Asset();
|
||||
$asset->model()->associate(AssetModel::find((int) $request->get('model_id')));
|
||||
|
||||
$asset->fill($request->validated());
|
||||
$asset->user_id = Auth::id();
|
||||
$asset->name = $request->get('name');
|
||||
$asset->serial = $request->get('serial');
|
||||
$asset->company_id = Company::getIdForCurrentUser($request->get('company_id'));
|
||||
$asset->model_id = $request->get('model_id');
|
||||
$asset->order_number = $request->get('order_number');
|
||||
$asset->notes = $request->get('notes');
|
||||
$asset->asset_tag = $request->get('asset_tag', Asset::autoincrement_asset());
|
||||
$asset->user_id = Auth::id();
|
||||
$asset->archived = '0';
|
||||
$asset->physical = '1';
|
||||
$asset->depreciate = '0';
|
||||
$asset->status_id = $request->get('status_id', 0);
|
||||
$asset->warranty_months = $request->get('warranty_months', null);
|
||||
$asset->purchase_cost = Helper::ParseCurrency($request->get('purchase_cost')); // this is the API's store method, so I don't know that I want to do this? Confusing. FIXME (or not?!)
|
||||
$asset->purchase_date = $request->get('purchase_date', null);
|
||||
$asset->assigned_to = $request->get('assigned_to', null);
|
||||
$asset->supplier_id = $request->get('supplier_id', 0);
|
||||
$asset->requestable = $request->get('requestable', 0);
|
||||
$asset->rtd_location_id = $request->get('rtd_location_id', null);
|
||||
$asset->location_id = $request->get('rtd_location_id', null);
|
||||
|
||||
/**
|
||||
* this is here just legacy reasons. Api\AssetController
|
||||
@@ -561,45 +537,38 @@ class AssetsController extends Controller
|
||||
$asset = $request->handleImages($asset);
|
||||
|
||||
// Update custom fields in the database.
|
||||
$model = AssetModel::find($request->input('model_id'));
|
||||
|
||||
// Check that it's an object and not a collection
|
||||
// (Sometimes people send arrays here and they shouldn't
|
||||
if (($model) && ($model instanceof AssetModel) && ($model->fieldset)) {
|
||||
// Validation for these fields is handled through the AssetRequest form request
|
||||
$model = AssetModel::find($request->get('model_id'));
|
||||
if (($model) && ($model->fieldset)) {
|
||||
foreach ($model->fieldset->fields as $field) {
|
||||
|
||||
// Set the field value based on what was sent in the request
|
||||
$field_val = $request->input($field->db_column, null);
|
||||
$field_val = $request->input($field->convertUnicodeDbSlug(), null);
|
||||
|
||||
// If input value is null, use custom field's default value
|
||||
if ($field_val == null) {
|
||||
Log::debug('Field value for '.$field->db_column.' is null');
|
||||
\Log::debug('Field value for '.$field->convertUnicodeDbSlug().' is null');
|
||||
$field_val = $field->defaultValue($request->get('model_id'));
|
||||
Log::debug('Use the default fieldset value of '.$field->defaultValue($request->get('model_id')));
|
||||
\Log::debug('Use the default fieldset value of '.$field->defaultValue($request->get('model_id')));
|
||||
}
|
||||
|
||||
// if the field is set to encrypted, make sure we encrypt the value
|
||||
if ($field->field_encrypted == '1') {
|
||||
Log::debug('This model field is encrypted in this fieldset.');
|
||||
\Log::debug('This model field is encrypted in this fieldset.');
|
||||
|
||||
if (Gate::allows('admin')) {
|
||||
|
||||
// If input value is null, use custom field's default value
|
||||
if (($field_val == null) && ($request->has('model_id') != '')) {
|
||||
$field_val = Crypt::encrypt($field->defaultValue($request->get('model_id')));
|
||||
$field_val = \Crypt::encrypt($field->defaultValue($request->get('model_id')));
|
||||
} else {
|
||||
$field_val = Crypt::encrypt($request->input($field->db_column));
|
||||
$field_val = \Crypt::encrypt($request->input($field->convertUnicodeDbSlug()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($field->element == 'checkbox') {
|
||||
if(is_array($field_val)) {
|
||||
$field_val = implode(',', $field_val);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$asset->{$field->db_column} = $field_val;
|
||||
$asset->{$field->convertUnicodeDbSlug()} = $field_val;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,8 +589,6 @@ class AssetsController extends Controller
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $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);
|
||||
@@ -634,7 +601,7 @@ class AssetsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param \App\Http\Requests\ImageUploadRequest $request
|
||||
* @since [v4.0]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function update(ImageUploadRequest $request, $id)
|
||||
{
|
||||
@@ -661,28 +628,18 @@ class AssetsController extends Controller
|
||||
$request->offsetSet('image', $request->offsetGet('image_source'));
|
||||
}
|
||||
|
||||
$asset = $request->handleImages($asset);
|
||||
$model = AssetModel::find($asset->model_id);
|
||||
$asset = $request->handleImages($asset);
|
||||
|
||||
// Update custom fields
|
||||
if (($model) && (isset($model->fieldset))) {
|
||||
if (($model = AssetModel::find($asset->model_id)) && (isset($model->fieldset))) {
|
||||
foreach ($model->fieldset->fields as $field) {
|
||||
$field_val = $request->input($field->db_column, null);
|
||||
|
||||
if ($request->has($field->db_column)) {
|
||||
if ($request->has($field->convertUnicodeDbSlug())) {
|
||||
if ($field->field_encrypted == '1') {
|
||||
if (Gate::allows('admin')) {
|
||||
$asset->{$field->db_column} = Crypt::encrypt($field_val);
|
||||
$asset->{$field->convertUnicodeDbSlug()} = \Crypt::encrypt($request->input($field->convertUnicodeDbSlug()));
|
||||
}
|
||||
}
|
||||
if ($field->element == 'checkbox') {
|
||||
if(is_array($field_val)) {
|
||||
$field_val = implode(',', $field_val);
|
||||
$asset->{$field->db_column} = $field_val;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$asset->{$field->db_column} = $field_val;
|
||||
} else {
|
||||
$asset->{$field->convertUnicodeDbSlug()} = $request->input($field->convertUnicodeDbSlug());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -710,7 +667,6 @@ class AssetsController extends Controller
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.success')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
|
||||
@@ -726,7 +682,7 @@ class AssetsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $assetId
|
||||
* @since [v4.0]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
@@ -755,45 +711,31 @@ class AssetsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $assetId
|
||||
* @since [v5.1.18]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function restore(Request $request, $assetId = null)
|
||||
public function restore($assetId = null)
|
||||
{
|
||||
// Get asset information
|
||||
$asset = Asset::withTrashed()->find($assetId);
|
||||
$this->authorize('delete', $asset);
|
||||
if (isset($asset->id)) {
|
||||
// Restore the asset
|
||||
Asset::withTrashed()->where('id', $assetId)->restore();
|
||||
|
||||
if ($asset = Asset::withTrashed()->find($assetId)) {
|
||||
$this->authorize('delete', $asset);
|
||||
$logaction = new Actionlog();
|
||||
$logaction->item_type = Asset::class;
|
||||
$logaction->item_id = $asset->id;
|
||||
$logaction->created_at = date("Y-m-d H:i:s");
|
||||
$logaction->user_id = Auth::user()->id;
|
||||
$logaction->logaction('restored');
|
||||
|
||||
if ($asset->deleted_at == '') {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', trans('general.not_deleted', ['item_type' => trans('general.asset')])), 200);
|
||||
}
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.restore.success')));
|
||||
|
||||
if ($asset->restore()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', trans('admin/hardware/message.restore.success')), 200);
|
||||
}
|
||||
|
||||
// Check validation to make sure we're not restoring an asset with the same asset tag (or unique attribute) as an existing asset
|
||||
return response()->json(Helper::formatStandardApiResponse('error', trans('general.could_not_restore', ['item_type' => trans('general.asset'), 'error' => $asset->getErrors()->first()])), 200);
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkout an asset by its tag.
|
||||
*
|
||||
* @author [N. Butler]
|
||||
* @param string $tag
|
||||
* @since [v6.0.5]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function checkoutByTag(AssetCheckoutRequest $request, $tag)
|
||||
{
|
||||
if ($asset = Asset::where('asset_tag', $tag)->first()) {
|
||||
return $this->checkout($request, $asset->id);
|
||||
}
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Asset not found'), 200);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checkout an asset
|
||||
@@ -801,7 +743,7 @@ class AssetsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $assetId
|
||||
* @since [v4.0]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function checkout(AssetCheckoutRequest $request, $asset_id)
|
||||
{
|
||||
@@ -830,6 +772,7 @@ class AssetsController extends Controller
|
||||
|
||||
} elseif (request('checkout_to_type') == 'asset') {
|
||||
$target = Asset::where('id', '!=', $asset_id)->find(request('assigned_asset'));
|
||||
$asset->location_id = $target->rtd_location_id;
|
||||
// Override with the asset's location_id if it has one
|
||||
$asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : '';
|
||||
$error_payload['target_id'] = $request->input('assigned_asset');
|
||||
@@ -843,9 +786,6 @@ class AssetsController extends Controller
|
||||
$error_payload['target_type'] = 'user';
|
||||
}
|
||||
|
||||
if ($request->filled('status_id')) {
|
||||
$asset->status_id = $request->get('status_id');
|
||||
}
|
||||
|
||||
|
||||
if (! isset($target)) {
|
||||
@@ -857,8 +797,7 @@ class AssetsController extends Controller
|
||||
$checkout_at = request('checkout_at', date('Y-m-d H:i:s'));
|
||||
$expected_checkin = request('expected_checkin', null);
|
||||
$note = request('note', null);
|
||||
// Using `->has` preserves the asset name if the name parameter was not included in request.
|
||||
$asset_name = request()->has('name') ? request('name') : $asset->name;
|
||||
$asset_name = request('name', null);
|
||||
|
||||
// Set the location ID to the RTD location id if there is one
|
||||
// Wait, why are we doing this? This overrides the stuff we set further up, which makes no sense.
|
||||
@@ -885,80 +824,49 @@ class AssetsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $assetId
|
||||
* @since [v4.0]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function checkin(Request $request, $asset_id)
|
||||
{
|
||||
$asset = Asset::with('model')->findOrFail($asset_id);
|
||||
$this->authorize('checkin', Asset::class);
|
||||
$asset = Asset::findOrFail($asset_id);
|
||||
$this->authorize('checkin', $asset);
|
||||
|
||||
|
||||
$target = $asset->assignedTo;
|
||||
if (is_null($target)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', [
|
||||
'asset_tag'=> e($asset->asset_tag),
|
||||
'model' => e($asset->model->name),
|
||||
'model_number' => e($asset->model->model_number)
|
||||
], trans('admin/hardware/message.checkin.already_checked_in')));
|
||||
return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.already_checked_in')));
|
||||
}
|
||||
|
||||
$asset->expected_checkin = null;
|
||||
//$asset->last_checkout = null;
|
||||
$asset->last_checkin = now();
|
||||
$asset->last_checkout = null;
|
||||
$asset->assigned_to = null;
|
||||
$asset->assignedTo()->disassociate($asset);
|
||||
$asset->accepted = null;
|
||||
|
||||
if ($request->has('name')) {
|
||||
if ($request->filled('name')) {
|
||||
$asset->name = $request->input('name');
|
||||
}
|
||||
|
||||
$this->migrateLegacyLocations($asset);
|
||||
|
||||
$asset->location_id = $asset->rtd_location_id;
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$asset->location_id = $request->input('location_id');
|
||||
|
||||
if ($request->input('update_default_location')){
|
||||
$asset->rtd_location_id = $request->input('location_id');
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('status_id')) {
|
||||
$asset->status_id = $request->input('status_id');
|
||||
}
|
||||
|
||||
$checkin_at = $request->filled('checkin_at') ? $request->input('checkin_at').' '. date('H:i:s') : date('Y-m-d H:i:s');
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
|
||||
if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) {
|
||||
$originalValues['action_date'] = $checkin_at;
|
||||
$checkin_at = null;
|
||||
if ($request->filled('checkin_at')) {
|
||||
$checkin_at = $request->input('checkin_at');
|
||||
}
|
||||
|
||||
$asset->licenseseats->each(function (LicenseSeat $seat) {
|
||||
$seat->update(['assigned_to' => null]);
|
||||
});
|
||||
|
||||
// Get all pending Acceptances for this asset and delete them
|
||||
CheckoutAcceptance::pending()
|
||||
->whereHasMorph(
|
||||
'checkoutable',
|
||||
[Asset::class],
|
||||
function (Builder $query) use ($asset) {
|
||||
$query->where('id', $asset->id);
|
||||
})
|
||||
->get()
|
||||
->map(function ($acceptance) {
|
||||
$acceptance->delete();
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', [
|
||||
'asset_tag'=> e($asset->asset_tag),
|
||||
'model' => e($asset->model->name),
|
||||
'model_number' => e($asset->model->model_number)
|
||||
], trans('admin/hardware/message.checkin.success')));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.success')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.error')));
|
||||
@@ -969,23 +877,20 @@ class AssetsController extends Controller
|
||||
*
|
||||
* @author [A. Janes] [<ajanes@adagiohealth.org>]
|
||||
* @since [v6.0]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function checkinByTag(Request $request, $tag = null)
|
||||
public function checkinByTag(Request $request)
|
||||
{
|
||||
$this->authorize('checkin', Asset::class);
|
||||
if(null == $tag && null !== ($request->input('asset_tag'))) {
|
||||
$tag = $request->input('asset_tag');
|
||||
}
|
||||
$asset = Asset::where('asset_tag', $tag)->first();
|
||||
$asset = Asset::where('asset_tag', $request->input('asset_tag'))->first();
|
||||
|
||||
if ($asset) {
|
||||
if($asset) {
|
||||
return $this->checkin($request, $asset->id);
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', [
|
||||
'asset'=> e($tag)
|
||||
], 'Asset with tag '.e($tag).' not found'));
|
||||
'asset'=> e($request->input('asset_tag'))
|
||||
], 'Asset with tag '.e($request->input('asset_tag')).' not found'));
|
||||
}
|
||||
|
||||
|
||||
@@ -995,7 +900,7 @@ class AssetsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $id
|
||||
* @since [v4.0]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function audit(Request $request)
|
||||
|
||||
@@ -1056,52 +961,22 @@ class AssetsController extends Controller
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v4.0]
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function requestable(Request $request)
|
||||
{
|
||||
$this->authorize('viewRequestable', Asset::class);
|
||||
|
||||
$allowed_columns = [
|
||||
'name',
|
||||
'asset_tag',
|
||||
'serial',
|
||||
'model_number',
|
||||
'image',
|
||||
'purchase_cost',
|
||||
'expected_checkin',
|
||||
];
|
||||
|
||||
$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();
|
||||
}
|
||||
|
||||
$assets = Asset::select('assets.*')
|
||||
->with('location', 'assetstatus', 'assetlog', 'company','assignedTo',
|
||||
'model.category', 'model.manufacturer', 'model.fieldset', 'supplier', 'requests');
|
||||
|
||||
|
||||
|
||||
$assets = Company::scopeCompanyables(Asset::select('assets.*'), 'company_id', 'assets')
|
||||
->with('location', 'assetstatus', 'assetlog', 'company', 'defaultLoc','assignedTo',
|
||||
'model.category', 'model.manufacturer', 'model.fieldset', 'supplier')->requestableAssets();
|
||||
|
||||
$offset = request('offset', 0);
|
||||
$limit = $request->input('limit', 50);
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
if ($request->filled('search')) {
|
||||
$assets->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
// Search custom fields by column name
|
||||
foreach ($all_custom_fields as $field) {
|
||||
if ($request->filled($field->db_column_name())) {
|
||||
$assets->where($field->db_column_name(), '=', $request->input($field->db_column_name()));
|
||||
}
|
||||
}
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort_override = str_replace('custom_fields.', '', $request->input('sort'));
|
||||
|
||||
// This handles all the pivot sorting (versus the assets.* fields
|
||||
// in the allowed_columns array)
|
||||
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.created_at';
|
||||
|
||||
switch ($request->input('sort')) {
|
||||
case 'model':
|
||||
@@ -1110,20 +985,17 @@ class AssetsController extends Controller
|
||||
case 'model_number':
|
||||
$assets->OrderModelNumber($order);
|
||||
break;
|
||||
case 'location':
|
||||
$assets->OrderLocation($order);
|
||||
case 'category':
|
||||
$assets->OrderCategory($order);
|
||||
break;
|
||||
case 'manufacturer':
|
||||
$assets->OrderManufacturer($order);
|
||||
break;
|
||||
default:
|
||||
$assets->orderBy($column_sort, $order);
|
||||
$assets->orderBy('assets.created_at', $order);
|
||||
break;
|
||||
}
|
||||
|
||||
$assets->requestableAssets();
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $assets->count()) ? $assets->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
|
||||
$total = $assets->count();
|
||||
$assets = $assets->skip($offset)->take($limit)->get();
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ use App\Models\Category;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class CategoriesController extends Controller
|
||||
{
|
||||
@@ -24,76 +23,21 @@ class CategoriesController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('view', Category::class);
|
||||
$allowed_columns = [
|
||||
'id',
|
||||
'name',
|
||||
'category_type',
|
||||
'category_type',
|
||||
'use_default_eula',
|
||||
'eula_text',
|
||||
'require_acceptance',
|
||||
'checkin_email',
|
||||
'assets_count',
|
||||
'accessories_count',
|
||||
'consumables_count',
|
||||
'components_count',
|
||||
'licenses_count',
|
||||
'image',
|
||||
];
|
||||
$allowed_columns = ['id', 'name', 'category_type', 'category_type', 'use_default_eula', 'eula_text', 'require_acceptance', 'checkin_email', 'assets_count', 'accessories_count', 'consumables_count', 'components_count', 'licenses_count', 'image'];
|
||||
|
||||
$categories = Category::select([
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'name', 'category_type',
|
||||
'use_default_eula',
|
||||
'eula_text',
|
||||
'require_acceptance',
|
||||
'checkin_email',
|
||||
'image'
|
||||
])->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count');
|
||||
|
||||
|
||||
/*
|
||||
* 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
|
||||
* may actually need to fetch assets that are archived.
|
||||
*
|
||||
* @see \App\Models\Category::showableAssets()
|
||||
*/
|
||||
if ($request->input('archived')=='true') {
|
||||
$categories = $categories->withCount('assets as assets_count');
|
||||
} else {
|
||||
$categories = $categories->withCount('showableAssets as assets_count');
|
||||
}
|
||||
$categories = Category::select(['id', 'created_at', 'updated_at', 'name', 'category_type', 'use_default_eula', 'eula_text', 'require_acceptance', 'checkin_email', 'image'])
|
||||
->withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$categories = $categories->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$categories->where('name', '=', $request->input('name'));
|
||||
}
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($categories) && ($request->get('offset') > $categories->count())) ? $categories->count() : $request->get('offset', 0);
|
||||
|
||||
if ($request->filled('category_type')) {
|
||||
$categories->where('category_type', '=', $request->input('category_type'));
|
||||
}
|
||||
|
||||
if ($request->filled('use_default_eula')) {
|
||||
$categories->where('use_default_eula', '=', $request->input('use_default_eula'));
|
||||
}
|
||||
|
||||
if ($request->filled('require_acceptance')) {
|
||||
$categories->where('require_acceptance', '=', $request->input('require_acceptance'));
|
||||
}
|
||||
|
||||
if ($request->filled('checkin_email')) {
|
||||
$categories->where('checkin_email', '=', $request->input('checkin_email'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $categories->count()) ? $categories->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'assets_count';
|
||||
@@ -141,7 +85,7 @@ class CategoriesController extends Controller
|
||||
public function show($id)
|
||||
{
|
||||
$this->authorize('view', Category::class);
|
||||
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count')->findOrFail($id);
|
||||
$category = Category::findOrFail($id);
|
||||
return (new CategoriesTransformer)->transformCategory($category);
|
||||
|
||||
}
|
||||
@@ -160,14 +104,8 @@ class CategoriesController extends Controller
|
||||
{
|
||||
$this->authorize('update', Category::class);
|
||||
$category = Category::findOrFail($id);
|
||||
|
||||
// Don't allow the user to change the category_type once it's been created
|
||||
if (($request->filled('category_type')) && ($category->category_type != $request->input('category_type'))) {
|
||||
return response()->json(
|
||||
Helper::formatStandardApiResponse('error', null, trans('admin/categories/message.update.cannot_change_category_type'))
|
||||
);
|
||||
}
|
||||
$category->fill($request->all());
|
||||
$category->category_type = strtolower($request->input('category_type'));
|
||||
$category = $request->handleImages($category);
|
||||
|
||||
if ($category->save()) {
|
||||
@@ -188,7 +126,7 @@ class CategoriesController extends Controller
|
||||
public function destroy($id)
|
||||
{
|
||||
$this->authorize('delete', Category::class);
|
||||
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count')->findOrFail($id);
|
||||
$category = Category::findOrFail($id);
|
||||
|
||||
if (! $category->isDeletable()) {
|
||||
return response()->json(
|
||||
|
||||
@@ -27,9 +27,6 @@ class CompaniesController extends Controller
|
||||
$allowed_columns = [
|
||||
'id',
|
||||
'name',
|
||||
'phone',
|
||||
'fax',
|
||||
'email',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'users_count',
|
||||
@@ -40,27 +37,18 @@ class CompaniesController extends Controller
|
||||
'components_count',
|
||||
];
|
||||
|
||||
$companies = Company::withCount(['assets as assets_count' => function ($query) {
|
||||
$query->AssetsForShow();
|
||||
}])->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
|
||||
$companies = Company::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$companies->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$companies->where('name', '=', $request->input('name'));
|
||||
}
|
||||
|
||||
if ($request->filled('email')) {
|
||||
$companies->where('email', '=', $request->input('email'));
|
||||
}
|
||||
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $companies->count()) ? $companies->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($companies) && ($request->get('offset') > $companies->count())) ? $companies->count() : $request->get('offset', 0);
|
||||
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
@@ -175,7 +163,6 @@ class CompaniesController extends Controller
|
||||
$companies = Company::select([
|
||||
'companies.id',
|
||||
'companies.name',
|
||||
'companies.email',
|
||||
'companies.image',
|
||||
]);
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ use App\Http\Requests\ImageUploadRequest;
|
||||
use App\Events\CheckoutableCheckedIn;
|
||||
use App\Events\ComponentCheckedIn;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
class ComponentsController extends Controller
|
||||
{
|
||||
@@ -45,17 +43,14 @@ class ComponentsController extends Controller
|
||||
'notes',
|
||||
];
|
||||
|
||||
$components = Component::select('components.*')
|
||||
->with('company', 'location', 'category', 'assets', 'supplier');
|
||||
|
||||
$components = Company::scopeCompanyables(Component::select('components.*')
|
||||
->with('company', 'location', 'category', 'assets'));
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$components = $components->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$components->where('name', '=', $request->input('name'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$components->where('company_id', '=', $request->input('company_id'));
|
||||
}
|
||||
@@ -64,10 +59,6 @@ class ComponentsController extends Controller
|
||||
$components->where('category_id', '=', $request->input('category_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$components->where('supplier_id', '=', $request->input('supplier_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$components->where('location_id', '=', $request->input('location_id'));
|
||||
}
|
||||
@@ -76,10 +67,14 @@ class ComponentsController extends Controller
|
||||
$components->where('notes','=',$request->input('notes'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $components->count()) ? $components->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($components) && ($request->get('offset') > $components->count())) ? $components->count() : $request->get('offset', 0);
|
||||
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort_override = $request->input('sort');
|
||||
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'created_at';
|
||||
@@ -94,9 +89,6 @@ class ComponentsController extends Controller
|
||||
case 'company':
|
||||
$components = $components->OrderCompany($order);
|
||||
break;
|
||||
case 'supplier':
|
||||
$components = $components->OrderSupplier($order);
|
||||
break;
|
||||
default:
|
||||
$components = $components->orderBy($column_sort, $order);
|
||||
break;
|
||||
@@ -204,29 +196,12 @@ class ComponentsController extends Controller
|
||||
$this->authorize('view', \App\Models\Asset::class);
|
||||
|
||||
$component = Component::findOrFail($id);
|
||||
|
||||
$assets = $component->assets();
|
||||
|
||||
$offset = request('offset', 0);
|
||||
$limit = $request->input('limit', 50);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$assets = $component->assets()
|
||||
->where(function ($query) use ($request) {
|
||||
$search_str = '%' . $request->input('search') . '%';
|
||||
$query->where('name', 'like', $search_str)
|
||||
->orWhereIn('model_id', function (Builder $query) use ($request) {
|
||||
$search_str = '%' . $request->input('search') . '%';
|
||||
$query->selectRaw('id')->from('models')->where('name', 'like', $search_str);
|
||||
})
|
||||
->orWhere('asset_tag', 'like', $search_str);
|
||||
})
|
||||
->get();
|
||||
$total = $assets->count();
|
||||
} else {
|
||||
$assets = $component->assets();
|
||||
|
||||
$total = $assets->count();
|
||||
$assets = $assets->skip($offset)->take($limit)->get();
|
||||
}
|
||||
$total = $assets->count();
|
||||
$assets = $assets->skip($offset)->take($limit)->get();
|
||||
|
||||
return (new ComponentsTransformer)->transformCheckedoutComponents($assets, $total);
|
||||
}
|
||||
@@ -246,30 +221,20 @@ class ComponentsController extends Controller
|
||||
public function checkout(Request $request, $componentId)
|
||||
{
|
||||
// Check if the component exists
|
||||
if (!$component = Component::find($componentId)) {
|
||||
if (is_null($component = Component::find($componentId))) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.does_not_exist')));
|
||||
}
|
||||
|
||||
$this->authorize('checkout', $component);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'assigned_to' => 'required|exists:assets,id',
|
||||
'assigned_qty' => "required|numeric|min:1|digits_between:1,".$component->numRemaining(),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', $validator->errors()));
|
||||
|
||||
}
|
||||
|
||||
// Make sure there is at least one available to checkout
|
||||
if ($component->numRemaining() < $request->get('assigned_qty')) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.checkout.unavailable', ['remaining' => $component->numRemaining(), 'requested' => $request->get('assigned_qty')])));
|
||||
}
|
||||
|
||||
if ($component->numRemaining() >= $request->get('assigned_qty')) {
|
||||
|
||||
$asset = Asset::find($request->input('assigned_to'));
|
||||
if (!$asset = Asset::find($request->input('assigned_to'))) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')));
|
||||
}
|
||||
|
||||
// Update the accessory data
|
||||
$component->assigned_to = $request->input('assigned_to');
|
||||
|
||||
$component->assets()->attach($component->id, [
|
||||
@@ -277,8 +242,7 @@ class ComponentsController extends Controller
|
||||
'created_at' => \Carbon::now(),
|
||||
'assigned_qty' => $request->get('assigned_qty', 1),
|
||||
'user_id' => \Auth::id(),
|
||||
'asset_id' => $request->get('assigned_to'),
|
||||
'note' => $request->get('note'),
|
||||
'asset_id' => $request->get('assigned_to')
|
||||
]);
|
||||
|
||||
$component->logCheckout($request->input('note'), $asset);
|
||||
@@ -286,7 +250,7 @@ class ComponentsController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkout.success')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.checkout.unavailable', ['remaining' => $component->numRemaining(), 'requested' => $request->get('assigned_qty')])));
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Not enough components remaining: '.$component->numRemaining().' remaining, '.$request->get('assigned_qty').' requested.'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Events\CheckoutableCheckedOut;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\ConsumablesTransformer;
|
||||
@@ -12,7 +11,6 @@ use App\Models\Consumable;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ConsumablesController extends Controller
|
||||
{
|
||||
@@ -47,17 +45,16 @@ class ConsumablesController extends Controller
|
||||
'notes',
|
||||
];
|
||||
|
||||
$consumables = Consumable::select('consumables.*')
|
||||
->with('company', 'location', 'category', 'users', 'manufacturer');
|
||||
|
||||
$consumables = Company::scopeCompanyables(
|
||||
Consumable::select('consumables.*')
|
||||
->with('company', 'location', 'category', 'users', 'manufacturer')
|
||||
);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$consumables = $consumables->TextSearch(e($request->input('search')));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$consumables->where('name', '=', $request->input('name'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$consumables->where('company_id', '=', $request->input('company_id'));
|
||||
}
|
||||
@@ -74,10 +71,6 @@ class ConsumablesController extends Controller
|
||||
$consumables->where('manufacturer_id', '=', $request->input('manufacturer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$consumables->where('supplier_id', '=', $request->input('supplier_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$consumables->where('location_id','=',$request->input('location_id'));
|
||||
}
|
||||
@@ -87,9 +80,12 @@ class ConsumablesController extends Controller
|
||||
}
|
||||
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $consumables->count()) ? $consumables->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($consumables) && ($request->get('offset') > $consumables->count())) ? $consumables->count() : $request->get('offset', 0);
|
||||
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$allowed_columns = ['id', 'name', 'order_number', 'min_amt', 'purchase_date', 'purchase_cost', 'company', 'category', 'model_number', 'item_no', 'manufacturer', 'location', 'qty', 'image'];
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
@@ -111,9 +107,6 @@ class ConsumablesController extends Controller
|
||||
case 'company':
|
||||
$consumables = $consumables->OrderCompany($order);
|
||||
break;
|
||||
case 'supplier':
|
||||
$components = $consumables->OrderSupplier($order);
|
||||
break;
|
||||
default:
|
||||
$consumables = $consumables->orderBy($column_sort, $order);
|
||||
break;
|
||||
@@ -157,7 +150,7 @@ class ConsumablesController extends Controller
|
||||
public function show($id)
|
||||
{
|
||||
$this->authorize('view', Consumable::class);
|
||||
$consumable = Consumable::with('users')->findOrFail($id);
|
||||
$consumable = Consumable::findOrFail($id);
|
||||
|
||||
return (new ConsumablesTransformer)->transformConsumable($consumable);
|
||||
}
|
||||
@@ -231,11 +224,9 @@ class ConsumablesController extends Controller
|
||||
|
||||
foreach ($consumable->consumableAssignments as $consumable_assignment) {
|
||||
$rows[] = [
|
||||
'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '',
|
||||
'name' => ($consumable_assignment->user) ? $consumable_assignment->user->present()->nameUrl() : 'Deleted User',
|
||||
'created_at' => Helper::getFormattedDateObject($consumable_assignment->created_at, 'datetime'),
|
||||
'note' => ($consumable_assignment->note) ? e($consumable_assignment->note) : null,
|
||||
'admin' => ($consumable_assignment->admin) ? $consumable_assignment->admin->present()->nameUrl() : null,
|
||||
'admin' => ($consumable_assignment->admin) ? $consumable_assignment->admin->present()->nameUrl() : '',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -256,46 +247,44 @@ class ConsumablesController extends Controller
|
||||
public function checkout(Request $request, $id)
|
||||
{
|
||||
// Check if the consumable exists
|
||||
if (!$consumable = Consumable::with('users')->find($id)) {
|
||||
if (is_null($consumable = Consumable::find($id))) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/consumables/message.does_not_exist')));
|
||||
}
|
||||
|
||||
$this->authorize('checkout', $consumable);
|
||||
|
||||
// Make sure there is at least one available to checkout
|
||||
if ($consumable->numRemaining() <= 0) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/consumables/message.checkout.unavailable')));
|
||||
if ($consumable->qty > 0) {
|
||||
|
||||
// Check if the user exists
|
||||
$assigned_to = $request->input('assigned_to');
|
||||
if (is_null($user = User::find($assigned_to))) {
|
||||
// Return error message
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'No user found'));
|
||||
}
|
||||
|
||||
// Update the consumable data
|
||||
$consumable->assigned_to = e($assigned_to);
|
||||
|
||||
$consumable->users()->attach($consumable->id, [
|
||||
'consumable_id' => $consumable->id,
|
||||
'user_id' => $user->id,
|
||||
'assigned_to' => $assigned_to,
|
||||
]);
|
||||
|
||||
// Log checkout event
|
||||
$logaction = $consumable->logCheckout(e($request->input('note')), $user);
|
||||
$data['log_id'] = $logaction->id;
|
||||
$data['eula'] = $consumable->getEula();
|
||||
$data['first_name'] = $user->first_name;
|
||||
$data['item_name'] = $consumable->name;
|
||||
$data['checkout_date'] = $logaction->created_at;
|
||||
$data['note'] = $logaction->note;
|
||||
$data['require_acceptance'] = $consumable->requireAcceptance();
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success')));
|
||||
}
|
||||
|
||||
// Make sure there is a valid category
|
||||
if (!$consumable->category){
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.invalid_item_category_single', ['type' => trans('general.consumable')])));
|
||||
}
|
||||
|
||||
|
||||
// Check if the user exists - @TODO: this should probably be handled via validation, not here??
|
||||
if (!$user = User::find($request->input('assigned_to'))) {
|
||||
// Return error message
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'No user found'));
|
||||
\Log::debug('No valid user');
|
||||
}
|
||||
|
||||
// Update the consumable data
|
||||
$consumable->assigned_to = $request->input('assigned_to');
|
||||
|
||||
$consumable->users()->attach($consumable->id,
|
||||
[
|
||||
'consumable_id' => $consumable->id,
|
||||
'user_id' => $user->id,
|
||||
'assigned_to' => $request->input('assigned_to'),
|
||||
'note' => $request->input('note'),
|
||||
]
|
||||
);
|
||||
|
||||
event(new CheckoutableCheckedOut($consumable, $user, Auth::user(), $request->input('note')));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/consumables/message.checkout.success')));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'No consumables remaining'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,7 +96,7 @@ class CustomFieldsController extends Controller
|
||||
$data = $request->all();
|
||||
$regex_format = null;
|
||||
|
||||
if ((array_key_exists('format', $data)) && (str_contains($data['format'], 'regex:'))) {
|
||||
if (str_contains($data['format'], 'regex:')) {
|
||||
$regex_format = $data['format'];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\CustomFieldsetsTransformer;
|
||||
use App\Http\Transformers\CustomFieldsTransformer;
|
||||
use App\Models\CustomFieldset;
|
||||
use App\Models\CustomField;
|
||||
use Illuminate\Http\Request;
|
||||
use Redirect;
|
||||
use View;
|
||||
@@ -34,7 +33,7 @@ class CustomFieldsetsController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->authorize('index', CustomField::class);
|
||||
$this->authorize('index', CustomFieldset::class);
|
||||
$fieldsets = CustomFieldset::withCount('fields as fields_count', 'models as models_count')->get();
|
||||
|
||||
return (new CustomFieldsetsTransformer)->transformCustomFieldsets($fieldsets, $fieldsets->count());
|
||||
@@ -50,7 +49,7 @@ class CustomFieldsetsController extends Controller
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
$this->authorize('view', CustomField::class);
|
||||
$this->authorize('view', CustomFieldset::class);
|
||||
if ($fieldset = CustomFieldset::find($id)) {
|
||||
return (new CustomFieldsetsTransformer)->transformCustomFieldset($fieldset);
|
||||
}
|
||||
@@ -69,7 +68,7 @@ class CustomFieldsetsController extends Controller
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$this->authorize('update', CustomField::class);
|
||||
$this->authorize('update', CustomFieldset::class);
|
||||
$fieldset = CustomFieldset::findOrFail($id);
|
||||
$fieldset->fill($request->all());
|
||||
|
||||
@@ -90,23 +89,11 @@ class CustomFieldsetsController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->authorize('create', CustomField::class);
|
||||
$this->authorize('create', CustomFieldset::class);
|
||||
$fieldset = new CustomFieldset;
|
||||
$fieldset->fill($request->all());
|
||||
|
||||
if ($fieldset->save()) {
|
||||
// Sync fieldset with auto_add_to_fieldsets
|
||||
$fields = CustomField::select('id')->where('auto_add_to_fieldsets', '=', '1')->get();
|
||||
|
||||
if ($fields->count() > 0) {
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$field_ids[] = $field->id;
|
||||
}
|
||||
|
||||
$fieldset->fields()->sync($field_ids);
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $fieldset, trans('admin/custom_fields/message.fieldset.create.success')));
|
||||
}
|
||||
|
||||
@@ -122,7 +109,7 @@ class CustomFieldsetsController extends Controller
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$this->authorize('delete', CustomField::class);
|
||||
$this->authorize('delete', CustomFieldset::class);
|
||||
$fieldset = CustomFieldset::findOrFail($id);
|
||||
|
||||
$modelsCount = $fieldset->models->count();
|
||||
@@ -149,7 +136,7 @@ class CustomFieldsetsController extends Controller
|
||||
*/
|
||||
public function fields($id)
|
||||
{
|
||||
$this->authorize('view', CustomField::class);
|
||||
$this->authorize('view', CustomFieldset::class);
|
||||
$set = CustomFieldset::findOrFail($id);
|
||||
$fields = $set->fields;
|
||||
|
||||
@@ -166,7 +153,7 @@ class CustomFieldsetsController extends Controller
|
||||
*/
|
||||
public function fieldsWithDefaultValues($fieldsetId, $modelId)
|
||||
{
|
||||
$this->authorize('view', CustomField::class);
|
||||
$this->authorize('view', CustomFieldset::class);
|
||||
|
||||
$set = CustomFieldset::findOrFail($fieldsetId);
|
||||
|
||||
|
||||
@@ -27,42 +27,27 @@ class DepartmentsController extends Controller
|
||||
$this->authorize('view', Department::class);
|
||||
$allowed_columns = ['id', 'name', 'image', 'users_count'];
|
||||
|
||||
$departments = Department::select(
|
||||
$departments = Company::scopeCompanyables(Department::select(
|
||||
'departments.id',
|
||||
'departments.name',
|
||||
'departments.phone',
|
||||
'departments.fax',
|
||||
'departments.location_id',
|
||||
'departments.company_id',
|
||||
'departments.manager_id',
|
||||
'departments.created_at',
|
||||
'departments.updated_at',
|
||||
'departments.image'
|
||||
)->with('users')->with('location')->with('manager')->with('company')->withCount('users as users_count');
|
||||
'departments.image'),
|
||||
"company_id", "departments")->with('users')->with('location')->with('manager')->with('company')->withCount('users as users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$departments = $departments->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$departments->where('name', '=', $request->input('name'));
|
||||
}
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($departments) && ($request->get('offset') > $departments->count())) ? $departments->count() : $request->get('offset', 0);
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$departments->where('company_id', '=', $request->input('company_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('manager_id')) {
|
||||
$departments->where('manager_id', '=', $request->input('manager_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
$departments->where('location_id', '=', $request->input('location_id'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $departments->count()) ? $departments->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
|
||||
@@ -28,9 +28,12 @@ class DepreciationsController extends Controller
|
||||
$depreciations = $depreciations->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $depreciations->count()) ? $depreciations->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($depreciations) && ($request->get('offset') > $depreciations->count())) ? $depreciations->count() : $request->get('offset', 0);
|
||||
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
|
||||
@@ -7,8 +7,6 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\GroupsTransformer;
|
||||
use App\Models\Group;
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
|
||||
|
||||
class GroupsController extends Controller
|
||||
{
|
||||
@@ -21,24 +19,21 @@ class GroupsController extends Controller
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('superadmin');
|
||||
|
||||
$this->authorize('view', Group::class);
|
||||
$allowed_columns = ['id', 'name', 'created_at', 'users_count'];
|
||||
|
||||
$groups = Group::select('id', 'name', 'permissions', 'created_at', 'updated_at', 'created_by')->with('admin')->withCount('users as users_count');
|
||||
$groups = Group::select('id', 'name', 'permissions', 'created_at', 'updated_at')->withCount('users as users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$groups = $groups->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$groups->where('name', '=', $request->input('name'));
|
||||
}
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($groups) && ($request->get('offset') > $groups->count())) ? $groups->count() : $request->get('offset', 0);
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $groups->count()) ? $groups->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
@@ -60,12 +55,9 @@ class GroupsController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->authorize('superadmin');
|
||||
$this->authorize('create', Group::class);
|
||||
$group = new Group;
|
||||
|
||||
$group->name = $request->input('name');
|
||||
$group->created_by = Auth::user()->id;
|
||||
$group->permissions = json_encode($request->input('permissions')); // Todo - some JSON validation stuff here
|
||||
$group->fill($request->all());
|
||||
|
||||
if ($group->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $group, trans('admin/groups/message.create.success')));
|
||||
@@ -84,7 +76,7 @@ class GroupsController extends Controller
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
$this->authorize('superadmin');
|
||||
$this->authorize('view', Group::class);
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
return (new GroupsTransformer)->transformGroup($group);
|
||||
@@ -101,11 +93,9 @@ class GroupsController extends Controller
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$this->authorize('superadmin');
|
||||
$this->authorize('update', Group::class);
|
||||
$group = Group::findOrFail($id);
|
||||
|
||||
$group->name = $request->input('name');
|
||||
$group->permissions = $request->input('permissions'); // Todo - some JSON validation stuff here
|
||||
$group->fill($request->all());
|
||||
|
||||
if ($group->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $group, trans('admin/groups/message.update.success')));
|
||||
@@ -124,8 +114,9 @@ class GroupsController extends Controller
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$this->authorize('superadmin');
|
||||
$this->authorize('delete', Group::class);
|
||||
$group = Group::findOrFail($id);
|
||||
$this->authorize('delete', $group);
|
||||
$group->delete();
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/groups/message.delete.success')));
|
||||
|
||||
@@ -10,7 +10,6 @@ use App\Models\Asset;
|
||||
use App\Models\Company;
|
||||
use App\Models\Import;
|
||||
use Artisan;
|
||||
use Illuminate\Database\Eloquent\JsonEncodingException;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -36,7 +35,7 @@ class ImportController extends Controller
|
||||
* Process and store a CSV upload file.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store()
|
||||
{
|
||||
@@ -57,7 +56,7 @@ class ImportController extends Controller
|
||||
'text/tsv', ])) {
|
||||
$results['error'] = 'File type must be CSV. Uploaded file is '.$file->getMimeType();
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $results['error']), 422);
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $results['error']), 500);
|
||||
}
|
||||
|
||||
//TODO: is there a lighter way to do this?
|
||||
@@ -65,19 +64,7 @@ class ImportController extends Controller
|
||||
ini_set('auto_detect_line_endings', '1');
|
||||
}
|
||||
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
|
||||
|
||||
try {
|
||||
$import->header_row = $reader->fetchOne(0);
|
||||
} catch (JsonEncodingException $e) {
|
||||
return response()->json(
|
||||
Helper::formatStandardApiResponse(
|
||||
'error',
|
||||
null,
|
||||
trans('admin/hardware/message.import.header_row_has_malformed_characters')
|
||||
),
|
||||
422
|
||||
);
|
||||
}
|
||||
$import->header_row = $reader->fetchOne(0);
|
||||
|
||||
//duplicate headers check
|
||||
$duplicate_headers = [];
|
||||
@@ -95,22 +82,11 @@ class ImportController extends Controller
|
||||
}
|
||||
}
|
||||
if (count($duplicate_headers) > 0) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, implode('; ', $duplicate_headers)),422);
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, implode('; ', $duplicate_headers)), 500); //should this be '4xx'?
|
||||
}
|
||||
|
||||
try {
|
||||
// Grab the first row to display via ajax as the user picks fields
|
||||
$import->first_row = $reader->fetchOne(1);
|
||||
} catch (JsonEncodingException $e) {
|
||||
return response()->json(
|
||||
Helper::formatStandardApiResponse(
|
||||
'error',
|
||||
null,
|
||||
trans('admin/hardware/message.import.content_row_has_malformed_characters')
|
||||
),
|
||||
422
|
||||
);
|
||||
}
|
||||
// Grab the first row to display via ajax as the user picks fields
|
||||
$import->first_row = $reader->fetchOne(1);
|
||||
|
||||
$date = date('Y-m-d-his');
|
||||
$fixed_filename = str_slug($file->getClientOriginalName());
|
||||
@@ -126,25 +102,18 @@ class ImportController extends Controller
|
||||
}
|
||||
$file_name = date('Y-m-d-his').'-'.$fixed_filename;
|
||||
$import->file_path = $file_name;
|
||||
$import->filesize = null;
|
||||
|
||||
if (!file_exists($path.'/'.$file_name)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')), 500);
|
||||
}
|
||||
|
||||
$import->filesize = filesize($path.'/'.$file_name);
|
||||
|
||||
$import->save();
|
||||
$results[] = $import;
|
||||
}
|
||||
$results = (new ImportsTransformer)->transformImports($results);
|
||||
|
||||
return response()->json([
|
||||
return [
|
||||
'files' => $results,
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.feature_disabled')), 422);
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.feature_disabled')), 500);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,21 +127,14 @@ class ImportController extends Controller
|
||||
$this->authorize('import');
|
||||
|
||||
// Run a backup immediately before processing
|
||||
if ($request->get('run-backup')) {
|
||||
if ($request->has('run-backup')) {
|
||||
\Log::debug('Backup manually requested via importer');
|
||||
Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H:i:s')]);
|
||||
Artisan::call('backup:run');
|
||||
} else {
|
||||
\Log::debug('NO BACKUP requested via importer');
|
||||
}
|
||||
|
||||
$import = Import::find($import_id);
|
||||
|
||||
if(is_null($import)){
|
||||
$error[0][0] = trans("validation.exists", ["attribute" => "file"]);
|
||||
return response()->json(Helper::formatStandardApiResponse('import-errors', null, $error), 500);
|
||||
}
|
||||
|
||||
$errors = $request->import($import);
|
||||
$errors = $request->import(Import::find($import_id));
|
||||
$redirectTo = 'hardware.index';
|
||||
switch ($request->get('import-type')) {
|
||||
case 'asset':
|
||||
@@ -193,9 +155,6 @@ class ImportController extends Controller
|
||||
case 'user':
|
||||
$redirectTo = 'users.index';
|
||||
break;
|
||||
case 'location':
|
||||
$redirectTo = 'locations.index';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($errors) { //Failure
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\LabelsTransformer;
|
||||
use App\Models\Labels\Label;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\ItemNotFoundException;
|
||||
use Auth;
|
||||
|
||||
class LabelsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Returns JSON listing of all labels.
|
||||
*
|
||||
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('view', Label::class);
|
||||
|
||||
$labels = Label::find();
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->get('search');
|
||||
$labels = $labels->filter(function ($label, $index) use ($search) {
|
||||
return stripos($label->getName(), $search) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
$total = $labels->count();
|
||||
|
||||
$offset = $request->get('offset', 0);
|
||||
$offset = ($offset > $total) ? $total : $offset;
|
||||
|
||||
$maxLimit = config('app.max_results');
|
||||
$limit = $request->get('limit', $maxLimit);
|
||||
$limit = ($limit > $maxLimit) ? $maxLimit : $limit;
|
||||
|
||||
$labels = $labels->skip($offset)->take($limit);
|
||||
|
||||
return (new LabelsTransformer)->transformLabels($labels, $total, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns JSON with information about a label for detail view.
|
||||
*
|
||||
* @author Grant Le Roux <grant.leroux+snipe-it@gmail.com>
|
||||
* @param string $labelName
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function show(string $labelName)
|
||||
{
|
||||
$labelName = str_replace('/', '\\', $labelName);
|
||||
try {
|
||||
$label = Label::find($labelName);
|
||||
} catch(ItemNotFoundException $e) {
|
||||
return response()
|
||||
->json(
|
||||
Helper::formatStandardApiResponse('error', null, trans('admin/labels/message.does_not_exist')),
|
||||
404
|
||||
);
|
||||
}
|
||||
$this->authorize('view', $label);
|
||||
return (new LabelsTransformer)->transformLabel($label);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -39,15 +39,8 @@ class LicenseSeatsController extends Controller
|
||||
}
|
||||
|
||||
$total = $seats->count();
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $seats->count()) ? $seats->count() : app('api_offset_value');
|
||||
|
||||
if ($offset >= $total ){
|
||||
$offset = 0;
|
||||
}
|
||||
|
||||
$limit = app('api_limit_value');
|
||||
$offset = (($seats) && (request('offset') > $total)) ? 0 : request('offset', 0);
|
||||
$limit = request('limit', 50);
|
||||
|
||||
$seats = $seats->skip($offset)->take($limit)->get();
|
||||
|
||||
@@ -123,20 +116,16 @@ class LicenseSeatsController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
|
||||
}
|
||||
|
||||
// the logging functions expect only one "target". if both asset and user are present in the request,
|
||||
// we simply let assets take precedence over users...
|
||||
if ($licenseSeat->isDirty('assigned_to')) {
|
||||
$target = $is_checkin ? $oldUser : User::find($licenseSeat->assigned_to);
|
||||
}
|
||||
if ($licenseSeat->isDirty('asset_id')) {
|
||||
$target = $is_checkin ? $oldAsset : Asset::find($licenseSeat->asset_id);
|
||||
}
|
||||
|
||||
if (is_null($target)){
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Target not found'));
|
||||
}
|
||||
|
||||
if ($licenseSeat->save()) {
|
||||
// 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...
|
||||
$changes = $licenseSeat->getChanges();
|
||||
if (array_key_exists('assigned_to', $changes)) {
|
||||
$target = $is_checkin ? $oldUser : User::find($changes['assigned_to']);
|
||||
}
|
||||
if (array_key_exists('asset_id', $changes)) {
|
||||
$target = $is_checkin ? $oldAsset : Asset::find($changes['asset_id']);
|
||||
}
|
||||
|
||||
if ($is_checkin) {
|
||||
$licenseSeat->logCheckin($target, $request->input('note'));
|
||||
|
||||
@@ -26,8 +26,7 @@ class LicensesController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('view', License::class);
|
||||
|
||||
$licenses = License::with('company', 'manufacturer', 'supplier','category')->withCount('freeSeats as free_seats_count');
|
||||
$licenses = Company::scopeCompanyables(License::with('company', 'manufacturer', 'supplier', 'category')->withCount('freeSeats as free_seats_count'));
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
$licenses->where('company_id', '=', $request->input('company_id'));
|
||||
@@ -73,6 +72,9 @@ class LicensesController extends Controller
|
||||
$licenses->where('depreciation_id', '=', $request->input('depreciation_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('supplier_id')) {
|
||||
$licenses->where('supplier_id', '=', $request->input('supplier_id'));
|
||||
}
|
||||
|
||||
if (($request->filled('maintained')) && ($request->input('maintained')=='true')) {
|
||||
$licenses->where('maintained','=',1);
|
||||
@@ -94,9 +96,12 @@ class LicensesController extends Controller
|
||||
$licenses->onlyTrashed();
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $licenses->count()) ? $licenses->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($licenses) && ($request->get('offset') > $licenses->count())) ? $licenses->count() : $request->get('offset', 0);
|
||||
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
@@ -136,7 +141,6 @@ class LicensesController extends Controller
|
||||
'seats',
|
||||
'termination_date',
|
||||
'depreciation_id',
|
||||
'min_amt',
|
||||
];
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
|
||||
$licenses = $licenses->orderBy($sort, $order);
|
||||
@@ -144,10 +148,9 @@ class LicensesController extends Controller
|
||||
}
|
||||
|
||||
$total = $licenses->count();
|
||||
|
||||
$licenses = $licenses->skip($offset)->take($limit)->get();
|
||||
return (new LicensesTransformer)->transformLicenses($licenses, $total);
|
||||
|
||||
return (new LicensesTransformer)->transformLicenses($licenses, $total);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,27 +25,9 @@ class LocationsController extends Controller
|
||||
{
|
||||
$this->authorize('view', Location::class);
|
||||
$allowed_columns = [
|
||||
'id',
|
||||
'name',
|
||||
'address',
|
||||
'address2',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
'zip',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'manager_id',
|
||||
'image',
|
||||
'assigned_assets_count',
|
||||
'users_count',
|
||||
'assets_count',
|
||||
'assigned_assets_count',
|
||||
'assets_count',
|
||||
'rtd_assets_count',
|
||||
'currency',
|
||||
'ldap_ou',
|
||||
];
|
||||
'id', 'name', 'address', 'address2', 'city', 'state', 'country', 'zip', 'created_at',
|
||||
'updated_at', 'manager_id', 'image',
|
||||
'assigned_assets_count', 'users_count', 'assets_count', 'currency', 'ldap_ou', ];
|
||||
|
||||
$locations = Location::with('parent', 'manager', 'children')->select([
|
||||
'locations.id',
|
||||
@@ -55,8 +37,6 @@ class LocationsController extends Controller
|
||||
'locations.city',
|
||||
'locations.state',
|
||||
'locations.zip',
|
||||
'locations.phone',
|
||||
'locations.fax',
|
||||
'locations.country',
|
||||
'locations.parent_id',
|
||||
'locations.manager_id',
|
||||
@@ -67,51 +47,20 @@ class LocationsController extends Controller
|
||||
'locations.currency',
|
||||
])->withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('rtd_assets as rtd_assets_count')
|
||||
->withCount('children as children_count')
|
||||
->withCount('users as users_count');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$locations = $locations->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$locations->where('locations.name', '=', $request->input('name'));
|
||||
}
|
||||
$offset = (($locations) && (request('offset') > $locations->count())) ? $locations->count() : request('offset', 0);
|
||||
|
||||
if ($request->filled('address')) {
|
||||
$locations->where('locations.address', '=', $request->input('address'));
|
||||
}
|
||||
|
||||
if ($request->filled('address2')) {
|
||||
$locations->where('locations.address2', '=', $request->input('address2'));
|
||||
}
|
||||
|
||||
if ($request->filled('city')) {
|
||||
$locations->where('locations.city', '=', $request->input('city'));
|
||||
}
|
||||
|
||||
if ($request->filled('zip')) {
|
||||
$locations->where('locations.zip', '=', $request->input('zip'));
|
||||
}
|
||||
|
||||
if ($request->filled('country')) {
|
||||
$locations->where('locations.country', '=', $request->input('country'));
|
||||
}
|
||||
|
||||
if ($request->filled('manager_id')) {
|
||||
$locations->where('locations.manager_id', '=', $request->input('manager_id'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $locations->count()) ? $locations->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
|
||||
|
||||
|
||||
switch ($request->input('sort')) {
|
||||
case 'parent':
|
||||
$locations->OrderParent($order);
|
||||
@@ -184,9 +133,7 @@ class LocationsController extends Controller
|
||||
])
|
||||
->withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('rtd_assets as rtd_assets_count')
|
||||
->withCount('users as users_count')
|
||||
->findOrFail($id);
|
||||
->withCount('users as users_count')->findOrFail($id);
|
||||
|
||||
return (new LocationsTransformer)->transformLocation($location);
|
||||
}
|
||||
@@ -235,13 +182,7 @@ class LocationsController extends Controller
|
||||
public function destroy($id)
|
||||
{
|
||||
$this->authorize('delete', Location::class);
|
||||
$location = Location::withCount('assignedAssets as assigned_assets_count')
|
||||
->withCount('assets as assets_count')
|
||||
->withCount('rtd_assets as rtd_assets_count')
|
||||
->withCount('children as children_count')
|
||||
->withCount('users as users_count')
|
||||
->findOrFail($id);
|
||||
|
||||
$location = Location::findOrFail($id);
|
||||
if (! $location->isDeletable()) {
|
||||
return response()
|
||||
->json(Helper::formatStandardApiResponse('error', null, trans('admin/companies/message.assoc_users')));
|
||||
@@ -282,12 +223,8 @@ class LocationsController extends Controller
|
||||
*/
|
||||
public function selectlist(Request $request)
|
||||
{
|
||||
// If a user is in the process of editing their profile, as determined by the referrer,
|
||||
// then we check that they have permission to edit their own location.
|
||||
// Otherwise, we do our normal check that they can view select lists.
|
||||
$request->headers->get('referer') === route('profile')
|
||||
? $this->authorize('self.edit_location')
|
||||
: $this->authorize('view.selectlists');
|
||||
|
||||
$this->authorize('view.selectlists');
|
||||
|
||||
$locations = Location::select([
|
||||
'locations.id',
|
||||
|
||||
@@ -6,11 +6,9 @@ use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\ManufacturersTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ManufacturersController extends Controller
|
||||
@@ -25,10 +23,10 @@ class ManufacturersController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('view', Manufacturer::class);
|
||||
$allowed_columns = ['id', 'name', 'url', 'support_url', 'support_email', 'warranty_lookup_url', 'support_phone', 'created_at', 'updated_at', 'image', 'assets_count', 'consumables_count', 'components_count', 'licenses_count'];
|
||||
$allowed_columns = ['id', 'name', 'url', 'support_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'assets_count', 'consumables_count', 'components_count', 'licenses_count'];
|
||||
|
||||
$manufacturers = Manufacturer::select(
|
||||
['id', 'name', 'url', 'support_url', 'warranty_lookup_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'deleted_at']
|
||||
['id', 'name', 'url', 'support_url', 'support_email', 'support_phone', 'created_at', 'updated_at', 'image', 'deleted_at']
|
||||
)->withCount('assets as assets_count')->withCount('licenses as licenses_count')->withCount('consumables as consumables_count')->withCount('accessories as accessories_count');
|
||||
|
||||
if ($request->input('deleted') == 'true') {
|
||||
@@ -39,33 +37,12 @@ class ManufacturersController extends Controller
|
||||
$manufacturers = $manufacturers->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$manufacturers->where('name', '=', $request->input('name'));
|
||||
}
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($manufacturers) && ($request->get('offset') > $manufacturers->count())) ? $manufacturers->count() : $request->get('offset', 0);
|
||||
|
||||
if ($request->filled('url')) {
|
||||
$manufacturers->where('url', '=', $request->input('url'));
|
||||
}
|
||||
|
||||
if ($request->filled('support_url')) {
|
||||
$manufacturers->where('support_url', '=', $request->input('support_url'));
|
||||
}
|
||||
|
||||
if ($request->filled('warranty_lookup_url')) {
|
||||
$manufacturers->where('warranty_lookup_url', '=', $request->input('warranty_lookup_url'));
|
||||
}
|
||||
|
||||
if ($request->filled('support_phone')) {
|
||||
$manufacturers->where('support_phone', '=', $request->input('support_phone'));
|
||||
}
|
||||
|
||||
if ($request->filled('support_email')) {
|
||||
$manufacturers->where('support_email', '=', $request->input('support_email'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $manufacturers->count()) ? $manufacturers->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
@@ -161,44 +138,6 @@ class ManufacturersController extends Controller
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a given Manufacturer (mark as un-deleted)
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v6.3.4]
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function restore($id)
|
||||
{
|
||||
$this->authorize('delete', Manufacturer::class);
|
||||
|
||||
if ($manufacturer = Manufacturer::withTrashed()->find($id)) {
|
||||
|
||||
if ($manufacturer->deleted_at == '') {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', trans('general.not_deleted', ['item_type' => trans('general.manufacturer')])), 200);
|
||||
}
|
||||
|
||||
if ($manufacturer->restore()) {
|
||||
|
||||
$logaction = new Actionlog();
|
||||
$logaction->item_type = Manufacturer::class;
|
||||
$logaction->item_id = $manufacturer->id;
|
||||
$logaction->created_at = date('Y-m-d H:i:s');
|
||||
$logaction->user_id = Auth::user()->id;
|
||||
$logaction->logaction('restore');
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', trans('admin/manufacturers/message.restore.success')), 200);
|
||||
}
|
||||
|
||||
// Check validation to make sure we're not restoring an item with the same unique attributes as a non-deleted one
|
||||
return response()->json(Helper::formatStandardApiResponse('error', trans('general.could_not_restore', ['item_type' => trans('general.manufacturer'), 'error' => $manufacturer->getErrors()->first()])), 200);
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/manufacturers/message.does_not_exist')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a paginated collection for the select2 menus
|
||||
*
|
||||
|
||||
@@ -29,10 +29,8 @@ class PredefinedKitsController extends Controller
|
||||
$kits = $kits->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $kits->count()) ? $kits->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
|
||||
$offset = $request->input('offset', 0);
|
||||
$limit = $request->input('limit', 50);
|
||||
$order = $request->input('order') === 'desc' ? 'desc' : 'asc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'name';
|
||||
$kits->orderBy($sort, $order);
|
||||
|
||||
@@ -5,38 +5,10 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CheckoutRequest;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Passport\TokenRepository;
|
||||
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use App\Models\CustomField;
|
||||
use DB;
|
||||
use Auth;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* The token repository implementation.
|
||||
*
|
||||
* @var \Laravel\Passport\TokenRepository
|
||||
*/
|
||||
protected $tokenRepository;
|
||||
|
||||
/**
|
||||
* Create a controller instance.
|
||||
*
|
||||
* @param \Laravel\Passport\TokenRepository $tokenRepository
|
||||
* @param \Illuminate\Contracts\Validation\Factory $validation
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(TokenRepository $tokenRepository, ValidationFactory $validation)
|
||||
{
|
||||
$this->validation = $validation;
|
||||
$this->tokenRepository = $tokenRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of requested assets.
|
||||
*
|
||||
@@ -49,129 +21,25 @@ class ProfileController extends Controller
|
||||
{
|
||||
$checkoutRequests = CheckoutRequest::where('user_id', '=', Auth::user()->id)->get();
|
||||
|
||||
$results = array();
|
||||
$show_field = array();
|
||||
$showable_fields = array();
|
||||
$results = [];
|
||||
$results['total'] = $checkoutRequests->count();
|
||||
|
||||
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
|
||||
foreach ($all_custom_fields as $field) {
|
||||
if (($field->field_encrypted=='0') && ($field->show_in_requestable_list=='1')) {
|
||||
$showable_fields[] = $field->db_column_name();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($checkoutRequests as $checkoutRequest) {
|
||||
|
||||
// Make sure the asset and request still exist
|
||||
if ($checkoutRequest && $checkoutRequest->itemRequested()) {
|
||||
$assets = [
|
||||
'image' => e($checkoutRequest->itemRequested()->present()->getImageUrl()),
|
||||
'name' => e($checkoutRequest->itemRequested()->present()->name()),
|
||||
'type' => e($checkoutRequest->itemType()),
|
||||
'qty' => (int) $checkoutRequest->quantity,
|
||||
'location' => ($checkoutRequest->location()) ? e($checkoutRequest->location()->name) : null,
|
||||
$results['rows'][] = [
|
||||
'image' => $checkoutRequest->itemRequested()->present()->getImageUrl(),
|
||||
'name' => $checkoutRequest->itemRequested()->present()->name(),
|
||||
'type' => $checkoutRequest->itemType(),
|
||||
'qty' => $checkoutRequest->quantity,
|
||||
'location' => ($checkoutRequest->location()) ? $checkoutRequest->location()->name : null,
|
||||
'expected_checkin' => Helper::getFormattedDateObject($checkoutRequest->itemRequested()->expected_checkin, 'datetime'),
|
||||
'request_date' => Helper::getFormattedDateObject($checkoutRequest->created_at, 'datetime'),
|
||||
];
|
||||
|
||||
foreach ($showable_fields as $showable_field_name) {
|
||||
$show_field['custom_fields.'.$showable_field_name] = $checkoutRequest->itemRequested()->{$showable_field_name};
|
||||
}
|
||||
|
||||
// Merge the plain asset data and the custom fields data
|
||||
$results['rows'][] = array_merge($assets, $show_field);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete an API token
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v6.0.5]
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function createApiToken(Request $request) {
|
||||
|
||||
if (!Gate::allows('self.api')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$accessTokenName = $request->input('name', 'Auth Token');
|
||||
|
||||
if ($accessToken = Auth::user()->createToken($accessTokenName)->accessToken) {
|
||||
|
||||
// Get the ID so we can return that with the payload
|
||||
$token = DB::table('oauth_access_tokens')->where('user_id', '=', Auth::user()->id)->where('name','=',$accessTokenName)->orderBy('created_at', 'desc')->first();
|
||||
$accessTokenData['id'] = $token->id;
|
||||
$accessTokenData['token'] = $accessToken;
|
||||
$accessTokenData['name'] = $accessTokenName;
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $accessTokenData, 'Personal access token '.$accessTokenName.' created successfully'));
|
||||
}
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Token could not be created.'));
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete an API token
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v6.0.5]
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function deleteApiToken($tokenId) {
|
||||
|
||||
if (!Gate::allows('self.api')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$token = $this->tokenRepository->findForUser(
|
||||
$tokenId, Auth::user()->getAuthIdentifier()
|
||||
);
|
||||
|
||||
if (is_null($token)) {
|
||||
return new Response('', 404);
|
||||
}
|
||||
|
||||
$token->revoke();
|
||||
|
||||
return new Response('', Response::HTTP_NO_CONTENT);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show user's API tokens
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v6.0.5]
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function showApiTokens(Request $request) {
|
||||
|
||||
if (!Gate::allows('self.api')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$tokens = $this->tokenRepository->forUser(Auth::user()->getAuthIdentifier());
|
||||
$token_values = $tokens->load('client')->filter(function ($token) {
|
||||
return $token->client->personal_access_client && ! $token->revoked;
|
||||
})->values();
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $token_values, null));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class ReportsController extends Controller
|
||||
{
|
||||
$this->authorize('reports.view');
|
||||
|
||||
$actionlogs = Actionlog::with('item', 'user', 'admin', 'target', 'location');
|
||||
$actionlogs = Actionlog::with('item', 'user', 'target', 'location');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$actionlogs = $actionlogs->TextSearch(e($request->input('search')));
|
||||
@@ -32,34 +32,14 @@ class ReportsController extends Controller
|
||||
}
|
||||
|
||||
if (($request->filled('item_type')) && ($request->filled('item_id'))) {
|
||||
$actionlogs = $actionlogs->where(function($query) use ($request)
|
||||
{
|
||||
$query->where('item_id', '=', $request->input('item_id'))
|
||||
->where('item_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')))
|
||||
->orWhere(function($query) use ($request)
|
||||
{
|
||||
$query->where('target_id', '=', $request->input('item_id'))
|
||||
->where('target_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')));
|
||||
});
|
||||
});
|
||||
$actionlogs = $actionlogs->where('item_id', '=', $request->input('item_id'))
|
||||
->where('item_type', '=', 'App\\Models\\'.ucwords($request->input('item_type')));
|
||||
}
|
||||
|
||||
if ($request->filled('action_type')) {
|
||||
$actionlogs = $actionlogs->where('action_type', '=', $request->input('action_type'))->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
if ($request->filled('user_id')) {
|
||||
$actionlogs = $actionlogs->where('user_id', '=', $request->input('user_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('action_source')) {
|
||||
$actionlogs = $actionlogs->where('action_source', '=', $request->input('action_source'))->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
if ($request->filled('remote_ip')) {
|
||||
$actionlogs = $actionlogs->where('remote_ip', '=', $request->input('remote_ip'))->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
if ($request->filled('uploads')) {
|
||||
$actionlogs = $actionlogs->whereNotNull('filename')->orderBy('created_at', 'desc');
|
||||
}
|
||||
@@ -72,20 +52,13 @@ class ReportsController extends Controller
|
||||
'accept_signature',
|
||||
'action_type',
|
||||
'note',
|
||||
'remote_ip',
|
||||
'user_agent',
|
||||
'action_source',
|
||||
];
|
||||
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $actionlogs->count()) ? $actionlogs->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
|
||||
$order = ($request->input('order') == 'asc') ? 'asc' : 'desc';
|
||||
$offset = request('offset', 0);
|
||||
$limit = request('limit', 50);
|
||||
$total = $actionlogs->count();
|
||||
|
||||
$actionlogs = $actionlogs->orderBy($sort, $order)->skip($offset)->take($limit)->get();
|
||||
|
||||
return response()->json((new ActionlogsTransformer)->transformActionlogs($actionlogs, $total), 200, ['Content-Type' => 'application/json;charset=utf8'], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Transformers\DatatablesTransformer;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Ldap;
|
||||
@@ -21,7 +18,6 @@ use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use App\Http\Requests\SlackSettingsRequest;
|
||||
use App\Http\Transformers\LoginAttemptsTransformer;
|
||||
|
||||
|
||||
class SettingsController extends Controller
|
||||
@@ -143,12 +139,53 @@ class SettingsController extends Controller
|
||||
|
||||
}
|
||||
|
||||
public function slacktest(SlackSettingsRequest $request)
|
||||
{
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'slack_endpoint' => 'url|required_with:slack_channel|starts_with:https://hooks.slack.com/|nullable',
|
||||
'slack_channel' => 'required_with:slack_endpoint|starts_with:#|nullable',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['message' => 'Validation failed', 'errors' => $validator->errors()], 422);
|
||||
}
|
||||
|
||||
// If validation passes, continue to the curl request
|
||||
$slack = new Client([
|
||||
'base_url' => e($request->input('slack_endpoint')),
|
||||
'defaults' => [
|
||||
'exceptions' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$payload = json_encode(
|
||||
[
|
||||
'channel' => e($request->input('slack_channel')),
|
||||
'text' => trans('general.slack_test_msg'),
|
||||
'username' => e($request->input('slack_botname')),
|
||||
'icon_emoji' => ':heart:',
|
||||
]);
|
||||
|
||||
try {
|
||||
$slack->post($request->input('slack_endpoint'), ['body' => $payload]);
|
||||
return response()->json(['message' => 'Success'], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json(['message' => 'Please check the channel name and webhook endpoint URL ('.e($request->input('slack_endpoint')).'). Slack responded with: '.$e->getMessage()], 400);
|
||||
}
|
||||
|
||||
//}
|
||||
return response()->json(['message' => 'Something went wrong :( '], 400);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test the email configuration
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v3.0]
|
||||
* @return JsonResponse
|
||||
* @return Redirect
|
||||
*/
|
||||
public function ajaxTestEmail()
|
||||
{
|
||||
@@ -170,7 +207,7 @@ class SettingsController extends Controller
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v5.0.0]
|
||||
* @return JsonResponse
|
||||
* @return Response
|
||||
*/
|
||||
public function purgeBarcodes()
|
||||
{
|
||||
@@ -211,7 +248,7 @@ class SettingsController extends Controller
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v5.0.0]
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array | JsonResponse
|
||||
* @return array
|
||||
*/
|
||||
public function showLoginAttempts(Request $request)
|
||||
{
|
||||
@@ -227,99 +264,4 @@ class SettingsController extends Controller
|
||||
|
||||
return (new LoginAttemptsTransformer)->transformLoginAttempts($login_attempt_results, $total);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Lists backup files
|
||||
*
|
||||
* @author [A. Gianotto]
|
||||
* @return array | JsonResponse
|
||||
*/
|
||||
public function listBackups() {
|
||||
$settings = Setting::getSettings();
|
||||
$path = 'app/backups';
|
||||
$backup_files = Storage::files($path);
|
||||
$files_raw = [];
|
||||
$count = 0;
|
||||
|
||||
if (count($backup_files) > 0) {
|
||||
|
||||
for ($f = 0; $f < count($backup_files); $f++) {
|
||||
|
||||
// Skip dotfiles like .gitignore and .DS_STORE
|
||||
if ((substr(basename($backup_files[$f]), 0, 1) != '.')) {
|
||||
$file_timestamp = Storage::lastModified($backup_files[$f]);
|
||||
|
||||
$files_raw[] = [
|
||||
'filename' => basename($backup_files[$f]),
|
||||
'filesize' => Setting::fileSizeConvert(Storage::size($backup_files[$f])),
|
||||
'modified_value' => $file_timestamp,
|
||||
'modified_display' => date($settings->date_display_format.' '.$settings->time_display_format, $file_timestamp),
|
||||
'backup_url' => config('app.url').'/settings/backups/download/'.basename($backup_files[$f]),
|
||||
|
||||
];
|
||||
$count++;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$files = array_reverse($files_raw);
|
||||
return (new DatatablesTransformer)->transformDatatables($files, $count);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Downloads a backup file.
|
||||
* We use response()->download() here instead of Storage::download() because Storage::download()
|
||||
* exhausts memory on larger files.
|
||||
*
|
||||
* @author [A. Gianotto]
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
*/
|
||||
public function downloadBackup($file) {
|
||||
|
||||
$path = storage_path('app/backups');
|
||||
|
||||
if (Storage::exists('app/backups/'.$file)) {
|
||||
$headers = ['ContentType' => 'application/zip'];
|
||||
return response()->download($path.'/'.$file, $file, $headers);
|
||||
} else {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')), 404);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines and downloads the latest backup
|
||||
*
|
||||
* @author [A. Gianotto]
|
||||
* @since [v6.3.1]
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
*/
|
||||
public function downloadLatestBackup() {
|
||||
|
||||
$fileData = collect();
|
||||
foreach (Storage::files('app/backups') as $file) {
|
||||
if (pathinfo($file, PATHINFO_EXTENSION) == 'zip') {
|
||||
$fileData->push([
|
||||
'file' => $file,
|
||||
'date' => Storage::lastModified($file)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$newest = $fileData->sortByDesc('date')->first();
|
||||
if (Storage::exists($newest['file'])) {
|
||||
$headers = ['ContentType' => 'application/zip'];
|
||||
return response()->download(storage_path($newest['file']), basename($newest['file']), $headers);
|
||||
} else {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_not_found')), 404);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -5,13 +5,10 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Transformers\AssetsTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Http\Transformers\StatuslabelsTransformer;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Statuslabel;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Transformers\PieChartTransformer;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class StatuslabelsController extends Controller
|
||||
{
|
||||
@@ -33,27 +30,26 @@ class StatuslabelsController extends Controller
|
||||
$statuslabels = $statuslabels->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$statuslabels->where('name', '=', $request->input('name'));
|
||||
}
|
||||
|
||||
|
||||
// if a status_type is passed, filter by that
|
||||
if ($request->filled('status_type')) {
|
||||
if (strtolower($request->input('status_type')) == 'pending') {
|
||||
if (strtolower($request->input('status_type'))== 'pending') {
|
||||
$statuslabels = $statuslabels->Pending();
|
||||
} elseif (strtolower($request->input('status_type')) == 'archived') {
|
||||
} elseif (strtolower($request->input('status_type'))== 'archived') {
|
||||
$statuslabels = $statuslabels->Archived();
|
||||
} elseif (strtolower($request->input('status_type')) == 'deployable') {
|
||||
} elseif (strtolower($request->input('status_type'))== 'deployable') {
|
||||
$statuslabels = $statuslabels->Deployable();
|
||||
} elseif (strtolower($request->input('status_type')) == 'undeployable') {
|
||||
} elseif (strtolower($request->input('status_type'))== 'undeployable') {
|
||||
$statuslabels = $statuslabels->Undeployable();
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $statuslabels->count()) ? $statuslabels->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($statuslabels) && ($request->get('offset') > $statuslabels->count())) ? $statuslabels->count() : $request->get('offset', 0);
|
||||
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
@@ -98,8 +94,8 @@ class StatuslabelsController extends Controller
|
||||
if ($statuslabel->save()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('success', $statuslabel, trans('admin/statuslabels/message.create.success')));
|
||||
}
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $statuslabel->getErrors()));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $statuslabel->getErrors()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +114,6 @@ class StatuslabelsController extends Controller
|
||||
return (new StatuslabelsTransformer)->transformStatuslabel($statuslabel);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
@@ -135,7 +130,6 @@ class StatuslabelsController extends Controller
|
||||
|
||||
$request->except('deployable', 'pending', 'archived');
|
||||
|
||||
|
||||
if (! $request->filled('type')) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, 'Status label type is required.'));
|
||||
}
|
||||
@@ -181,62 +175,48 @@ class StatuslabelsController extends Controller
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/statuslabels/message.assoc_assets')));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Show a count of assets by status label for pie chart
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v3.0]
|
||||
* @return array
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function getAssetCountByStatuslabel()
|
||||
{
|
||||
$this->authorize('view', Statuslabel::class);
|
||||
|
||||
$statuslabels = Statuslabel::withCount('assets')->get();
|
||||
$total = Array();
|
||||
|
||||
$labels = [];
|
||||
$points = [];
|
||||
$default_color_count = 0;
|
||||
$colors_array = [];
|
||||
|
||||
foreach ($statuslabels as $statuslabel) {
|
||||
if ($statuslabel->assets_count > 0) {
|
||||
$labels[] = $statuslabel->name.' ('.number_format($statuslabel->assets_count).')';
|
||||
$points[] = $statuslabel->assets_count;
|
||||
|
||||
$total[$statuslabel->name]['label'] = $statuslabel->name;
|
||||
$total[$statuslabel->name]['count'] = $statuslabel->assets_count;
|
||||
|
||||
if ($statuslabel->color != '') {
|
||||
$total[$statuslabel->name]['color'] = $statuslabel->color;
|
||||
if ($statuslabel->color != '') {
|
||||
$colors_array[] = $statuslabel->color;
|
||||
} else {
|
||||
$colors_array[] = Helper::defaultChartColors($default_color_count);
|
||||
}
|
||||
$default_color_count++;
|
||||
}
|
||||
}
|
||||
|
||||
return (new PieChartTransformer())->transformPieChartDate($total);
|
||||
$result = [
|
||||
'labels' => $labels,
|
||||
'datasets' => [[
|
||||
'data' => $points,
|
||||
'backgroundColor' => $colors_array,
|
||||
'hoverBackgroundColor' => $colors_array,
|
||||
]],
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a count of assets by meta status type for pie chart
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v6.0.11]
|
||||
* @return array
|
||||
*/
|
||||
public function getAssetCountByMetaStatus()
|
||||
{
|
||||
$this->authorize('view', Statuslabel::class);
|
||||
|
||||
$total['rtd']['label'] = trans('general.ready_to_deploy');
|
||||
$total['rtd']['count'] = Asset::RTD()->count();
|
||||
|
||||
$total['deployed']['label'] = trans('general.deployed');
|
||||
$total['deployed']['count'] = Asset::Deployed()->count();
|
||||
|
||||
$total['archived']['label'] = trans('general.archived');
|
||||
$total['archived']['count'] = Asset::Archived()->count();
|
||||
|
||||
$total['pending']['label'] = trans('general.pending');
|
||||
$total['pending']['count'] = Asset::Pending()->count();
|
||||
|
||||
$total['undeployable']['label'] = trans('general.undeployable');
|
||||
$total['undeployable']['count'] = Asset::Undeployable()->count();
|
||||
|
||||
return (new PieChartTransformer())->transformPieChartDate($total);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,45 +272,4 @@ class StatuslabelsController extends Controller
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a paginated collection for the select2 menus
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v6.1.1]
|
||||
* @see \App\Http\Transformers\SelectlistTransformer
|
||||
*/
|
||||
public function selectlist(Request $request)
|
||||
{
|
||||
|
||||
$this->authorize('view.selectlists');
|
||||
$statuslabels = Statuslabel::orderBy('default_label', 'desc')->orderBy('name', 'asc')->orderBy('deployable', 'desc');
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$statuslabels = $statuslabels->where('name', 'LIKE', '%'.$request->get('search').'%');
|
||||
}
|
||||
|
||||
if ($request->filled('deployable')) {
|
||||
$statuslabels = $statuslabels->where('deployable', '=', '1');
|
||||
}
|
||||
|
||||
if ($request->filled('pending')) {
|
||||
$statuslabels = $statuslabels->where('pending', '=', '1');
|
||||
}
|
||||
|
||||
if ($request->filled('archived')) {
|
||||
$statuslabels = $statuslabels->where('archived', '=', '1');
|
||||
}
|
||||
|
||||
$statuslabels = $statuslabels->orderBy('name', 'ASC')->paginate(50);
|
||||
|
||||
// Loop through and set some custom properties for the transformer to use.
|
||||
// This lets us have more flexibility in special cases like assets, where
|
||||
// they may not have a ->name value but we want to display something anyway
|
||||
foreach ($statuslabels as $statuslabel) {
|
||||
$statuslabels->use_text = $statuslabel->name;
|
||||
}
|
||||
|
||||
return (new SelectlistTransformer)->transformSelectlist($statuslabels);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,79 +23,23 @@ class SuppliersController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authorize('view', Supplier::class);
|
||||
$allowed_columns = ['
|
||||
id',
|
||||
'name',
|
||||
'address',
|
||||
'phone',
|
||||
'contact',
|
||||
'fax',
|
||||
'email',
|
||||
'image',
|
||||
'assets_count',
|
||||
'licenses_count',
|
||||
'accessories_count',
|
||||
'components_count',
|
||||
'consumables_count',
|
||||
'url',
|
||||
];
|
||||
$allowed_columns = ['id', 'name', 'address', 'phone', 'contact', 'fax', 'email', 'image', 'assets_count', 'licenses_count', 'accessories_count', 'url'];
|
||||
|
||||
$suppliers = Supplier::select(
|
||||
['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('licenses as licenses_count')
|
||||
->withCount('accessories as accessories_count')
|
||||
->withCount('components as components_count')
|
||||
->withCount('consumables as consumables_count');
|
||||
['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'updated_at', 'deleted_at', 'image', 'notes']
|
||||
)->withCount('assets as assets_count')->withCount('licenses as licenses_count')->withCount('accessories as accessories_count');
|
||||
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$suppliers = $suppliers->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
if ($request->filled('name')) {
|
||||
$suppliers->where('name', '=', $request->input('name'));
|
||||
}
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($suppliers) && ($request->get('offset') > $suppliers->count())) ? $suppliers->count() : $request->get('offset', 0);
|
||||
|
||||
if ($request->filled('address')) {
|
||||
$suppliers->where('address', '=', $request->input('address'));
|
||||
}
|
||||
|
||||
if ($request->filled('address2')) {
|
||||
$suppliers->where('address2', '=', $request->input('address2'));
|
||||
}
|
||||
|
||||
if ($request->filled('city')) {
|
||||
$suppliers->where('city', '=', $request->input('city'));
|
||||
}
|
||||
|
||||
if ($request->filled('zip')) {
|
||||
$suppliers->where('zip', '=', $request->input('zip'));
|
||||
}
|
||||
|
||||
if ($request->filled('country')) {
|
||||
$suppliers->where('country', '=', $request->input('country'));
|
||||
}
|
||||
|
||||
if ($request->filled('fax')) {
|
||||
$suppliers->where('fax', '=', $request->input('fax'));
|
||||
}
|
||||
|
||||
if ($request->filled('email')) {
|
||||
$suppliers->where('email', '=', $request->input('email'));
|
||||
}
|
||||
|
||||
if ($request->filled('url')) {
|
||||
$suppliers->where('url', '=', $request->input('url'));
|
||||
}
|
||||
|
||||
if ($request->filled('notes')) {
|
||||
$suppliers->where('notes', '=', $request->input('notes'));
|
||||
}
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $suppliers->count()) ? $suppliers->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
|
||||
|
||||
@@ -11,17 +11,14 @@ use App\Http\Transformers\ConsumablesTransformer;
|
||||
use App\Http\Transformers\LicensesTransformer;
|
||||
use App\Http\Transformers\SelectlistTransformer;
|
||||
use App\Http\Transformers\UsersTransformer;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Company;
|
||||
use App\Models\License;
|
||||
use App\Models\User;
|
||||
use App\Notifications\CurrentInventory;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class UsersController extends Controller
|
||||
{
|
||||
@@ -39,7 +36,6 @@ class UsersController extends Controller
|
||||
|
||||
$users = User::select([
|
||||
'users.activated',
|
||||
'users.created_by',
|
||||
'users.address',
|
||||
'users.avatar',
|
||||
'users.city',
|
||||
@@ -69,16 +65,18 @@ class UsersController extends Controller
|
||||
'users.zip',
|
||||
'users.remote',
|
||||
'users.ldap_import',
|
||||
'users.start_date',
|
||||
'users.end_date',
|
||||
'users.vip',
|
||||
'users.autoassign_licenses',
|
||||
'users.website',
|
||||
|
||||
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy',)
|
||||
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables')
|
||||
->withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count');
|
||||
$users = Company::scopeCompanyables($users);
|
||||
|
||||
|
||||
if (($request->filled('deleted')) && ($request->input('deleted') == 'true')) {
|
||||
$users = $users->onlyTrashed();
|
||||
} elseif (($request->filled('all')) && ($request->input('all') == 'true')) {
|
||||
$users = $users->withTrashed();
|
||||
}
|
||||
|
||||
if ($request->filled('activated')) {
|
||||
$users = $users->where('users.activated', '=', $request->input('activated'));
|
||||
}
|
||||
@@ -91,10 +89,6 @@ class UsersController extends Controller
|
||||
$users = $users->where('users.location_id', '=', $request->input('location_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('created_by')) {
|
||||
$users = $users->where('users.created_by', '=', $request->input('created_by'));
|
||||
}
|
||||
|
||||
if ($request->filled('email')) {
|
||||
$users = $users->where('users.email', '=', $request->input('email'));
|
||||
}
|
||||
@@ -123,10 +117,6 @@ class UsersController extends Controller
|
||||
$users = $users->where('users.country', '=', $request->input('country'));
|
||||
}
|
||||
|
||||
if ($request->filled('website')) {
|
||||
$users = $users->where('users.website', '=', $request->input('website'));
|
||||
}
|
||||
|
||||
if ($request->filled('zip')) {
|
||||
$users = $users->where('users.zip', '=', $request->input('zip'));
|
||||
}
|
||||
@@ -151,26 +141,6 @@ class UsersController extends Controller
|
||||
$users = $users->where('remote', '=', $request->input('remote'));
|
||||
}
|
||||
|
||||
if ($request->filled('vip')) {
|
||||
$users = $users->where('vip', '=', $request->input('vip'));
|
||||
}
|
||||
|
||||
if ($request->filled('two_factor_enrolled')) {
|
||||
$users = $users->where('two_factor_enrolled', '=', $request->input('two_factor_enrolled'));
|
||||
}
|
||||
|
||||
if ($request->filled('two_factor_optin')) {
|
||||
$users = $users->where('two_factor_optin', '=', $request->input('two_factor_optin'));
|
||||
}
|
||||
|
||||
if ($request->filled('start_date')) {
|
||||
$users = $users->where('users.start_date', '=', $request->input('start_date'));
|
||||
}
|
||||
|
||||
if ($request->filled('end_date')) {
|
||||
$users = $users->where('users.end_date', '=', $request->input('end_date'));
|
||||
}
|
||||
|
||||
if ($request->filled('assets_count')) {
|
||||
$users->has('assets', '=', $request->input('assets_count'));
|
||||
}
|
||||
@@ -187,15 +157,20 @@ class UsersController extends Controller
|
||||
$users->has('accessories', '=', $request->input('accessories_count'));
|
||||
}
|
||||
|
||||
if ($request->filled('autoassign_licenses')) {
|
||||
$users->where('autoassign_licenses', '=', $request->input('autoassign_licenses'));
|
||||
}
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$users = $users->TextSearch($request->input('search'));
|
||||
}
|
||||
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$offset = (($users) && (request('offset') > $users->count())) ? 0 : request('offset', 0);
|
||||
|
||||
// Set the offset to the API call's offset, unless the offset is higher than the actual count of items in which
|
||||
// case we override with the actual count, so we should return 0 items.
|
||||
$offset = (($users) && ($request->get('offset') > $users->count())) ? $users->count() : $request->get('offset', 0);
|
||||
|
||||
// Check to make sure the limit is not higher than the max allowed
|
||||
((config('app.max_results') >= $request->input('limit')) && ($request->filled('limit'))) ? $limit = $request->input('limit') : $limit = config('app.max_results');
|
||||
|
||||
|
||||
switch ($request->input('sort')) {
|
||||
case 'manager':
|
||||
@@ -207,59 +182,17 @@ class UsersController extends Controller
|
||||
case 'department':
|
||||
$users = $users->OrderDepartment($order);
|
||||
break;
|
||||
case 'created_by':
|
||||
$users = $users->OrderByCreatedBy($order);
|
||||
break;
|
||||
case 'company':
|
||||
$users = $users->OrderCompany($order);
|
||||
break;
|
||||
case 'first_name':
|
||||
$users->orderBy('first_name', $order);
|
||||
$users->orderBy('last_name', $order);
|
||||
break;
|
||||
case 'last_name':
|
||||
$users->orderBy('last_name', $order);
|
||||
$users->orderBy('first_name', $order);
|
||||
break;
|
||||
default:
|
||||
$allowed_columns =
|
||||
[
|
||||
'last_name',
|
||||
'first_name',
|
||||
'email',
|
||||
'jobtitle',
|
||||
'username',
|
||||
'employee_num',
|
||||
'assets',
|
||||
'accessories',
|
||||
'consumables',
|
||||
'licenses',
|
||||
'groups',
|
||||
'activated',
|
||||
'created_at',
|
||||
'two_factor_enrolled',
|
||||
'two_factor_optin',
|
||||
'last_login',
|
||||
'assets_count',
|
||||
'licenses_count',
|
||||
'consumables_count',
|
||||
'accessories_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',
|
||||
'last_name', 'first_name', 'email', 'jobtitle', 'username', 'employee_num',
|
||||
'assets', 'accessories', 'consumables', 'licenses', 'groups', 'activated', 'created_at',
|
||||
'two_factor_enrolled', 'two_factor_optin', 'last_login', 'assets_count', 'licenses_count',
|
||||
'consumables_count', 'accessories_count', 'phone', 'address', 'city', 'state',
|
||||
'country', 'zip', 'id', 'ldap_import', 'remote',
|
||||
];
|
||||
|
||||
$sort = in_array($request->get('sort'), $allowed_columns) ? $request->get('sort') : 'first_name';
|
||||
@@ -267,19 +200,6 @@ class UsersController extends Controller
|
||||
break;
|
||||
}
|
||||
|
||||
if (($request->filled('deleted')) && ($request->input('deleted') == 'true')) {
|
||||
$users = $users->onlyTrashed();
|
||||
} elseif (($request->filled('all')) && ($request->input('all') == 'true')) {
|
||||
$users = $users->withTrashed();
|
||||
}
|
||||
|
||||
$users = Company::scopeCompanyables($users);
|
||||
|
||||
|
||||
// Make sure the offset and limit are actually integers and do not exceed system limits
|
||||
$offset = ($request->input('offset') > $users->count()) ? $users->count() : app('api_offset_value');
|
||||
$limit = app('api_limit_value');
|
||||
|
||||
$total = $users->count();
|
||||
$users = $users->skip($offset)->take($limit)->get();
|
||||
|
||||
@@ -311,11 +231,9 @@ class UsersController extends Controller
|
||||
$users = Company::scopeCompanyables($users);
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$users = $users->where(function ($query) use ($request) {
|
||||
$query->SimpleNameSearch($request->get('search'))
|
||||
->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
|
||||
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
|
||||
});
|
||||
$users = $users->SimpleNameSearch($request->get('search'))
|
||||
->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
|
||||
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
|
||||
}
|
||||
|
||||
$users = $users->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
|
||||
@@ -359,7 +277,6 @@ class UsersController extends Controller
|
||||
|
||||
$user = new User;
|
||||
$user->fill($request->all());
|
||||
$user->created_by = Auth::user()->id;
|
||||
|
||||
if ($request->has('permissions')) {
|
||||
$permissions_array = $request->input('permissions');
|
||||
@@ -371,12 +288,8 @@ class UsersController extends Controller
|
||||
$user->permissions = $permissions_array;
|
||||
}
|
||||
|
||||
//
|
||||
if ($request->filled('password')) {
|
||||
$user->password = bcrypt($request->get('password'));
|
||||
} else {
|
||||
$user->password = $user->noPassword();
|
||||
}
|
||||
$tmp_pass = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 20);
|
||||
$user->password = bcrypt($request->get('password', $tmp_pass));
|
||||
|
||||
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
|
||||
|
||||
@@ -481,13 +394,6 @@ class UsersController extends Controller
|
||||
|
||||
// Check if the request has groups passed and has a value
|
||||
if ($request->filled('groups')) {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'groups.*' => 'integer|exists:permission_groups,id',
|
||||
]);
|
||||
|
||||
if ($validator->fails()){
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
|
||||
}
|
||||
$user->groups()->sync($request->input('groups'));
|
||||
// The groups field has been passed but it is null, so we should blank it out
|
||||
} elseif ($request->has('groups')) {
|
||||
@@ -560,51 +466,11 @@ class UsersController extends Controller
|
||||
{
|
||||
$this->authorize('view', User::class);
|
||||
$this->authorize('view', Asset::class);
|
||||
$assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model');
|
||||
|
||||
|
||||
// Filter on category ID
|
||||
if ($request->filled('category_id')) {
|
||||
$assets = $assets->InCategory($request->input('category_id'));
|
||||
}
|
||||
|
||||
|
||||
// Filter on model ID
|
||||
if ($request->filled('model_id')) {
|
||||
|
||||
$model_ids = $request->input('model_id');
|
||||
if (!is_array($model_ids)) {
|
||||
$model_ids = array($model_ids);
|
||||
}
|
||||
$assets = $assets->InModelList($model_ids);
|
||||
}
|
||||
|
||||
$assets = $assets->get();
|
||||
$assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model')->get();
|
||||
|
||||
return (new AssetsTransformer)->transformAssets($assets, $assets->count(), $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify a specific user via email with all of their assigned assets.
|
||||
*
|
||||
* @author [Lukas Fehling] [<lukas.fehling@adabay.rocks>]
|
||||
* @since [v6.0.13]
|
||||
* @param Request $request
|
||||
* @param $id
|
||||
* @return string JSON
|
||||
*/
|
||||
public function emailAssetList(Request $request, $id)
|
||||
{
|
||||
$user = User::findOrFail($id);
|
||||
|
||||
if (empty($user->email)) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.inventorynotification.error')));
|
||||
}
|
||||
|
||||
$user->notify((new CurrentInventory($user)));
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.inventorynotification.success')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return JSON containing a list of consumables assigned to a user.
|
||||
@@ -653,14 +519,10 @@ class UsersController extends Controller
|
||||
{
|
||||
$this->authorize('view', User::class);
|
||||
$this->authorize('view', License::class);
|
||||
|
||||
if ($user = User::where('id', $id)->withTrashed()->first()) {
|
||||
$licenses = $user->licenses()->get();
|
||||
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
|
||||
$user = User::where('id', $id)->withTrashed()->first();
|
||||
$licenses = $user->licenses()->get();
|
||||
|
||||
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -680,17 +542,7 @@ class UsersController extends Controller
|
||||
$user = User::find($request->get('id'));
|
||||
$user->two_factor_secret = null;
|
||||
$user->two_factor_enrolled = 0;
|
||||
$user->saveQuietly();
|
||||
|
||||
// Log the reset
|
||||
$logaction = new Actionlog();
|
||||
$logaction->target_type = User::class;
|
||||
$logaction->target_id = $user->id;
|
||||
$logaction->item_type = User::class;
|
||||
$logaction->item_id = $user->id;
|
||||
$logaction->created_at = date('Y-m-d H:i:s');
|
||||
$logaction->user_id = Auth::user()->id;
|
||||
$logaction->logaction('2FA reset');
|
||||
$user->save();
|
||||
|
||||
return response()->json(['message' => trans('admin/settings/general.two_factor_reset_success')], 200);
|
||||
} catch (\Exception $e) {
|
||||
@@ -725,31 +577,17 @@ class UsersController extends Controller
|
||||
*/
|
||||
public function restore($userId = null)
|
||||
{
|
||||
// Get asset information
|
||||
$user = User::withTrashed()->find($userId);
|
||||
$this->authorize('delete', $user);
|
||||
if (isset($user->id)) {
|
||||
// Restore the user
|
||||
User::withTrashed()->where('id', $userId)->restore();
|
||||
|
||||
if ($user = User::withTrashed()->find($userId)) {
|
||||
$this->authorize('delete', $user);
|
||||
|
||||
if ($user->deleted_at == '') {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', trans('general.not_deleted', ['item_type' => trans('general.user')])), 200);
|
||||
}
|
||||
|
||||
if ($user->restore()) {
|
||||
|
||||
$logaction = new Actionlog();
|
||||
$logaction->item_type = User::class;
|
||||
$logaction->item_id = $user->id;
|
||||
$logaction->created_at = date('Y-m-d H:i:s');
|
||||
$logaction->user_id = Auth::user()->id;
|
||||
$logaction->logaction('restore');
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.restored')), 200);
|
||||
}
|
||||
|
||||
// Check validation to make sure we're not restoring a user with the same username as an existing user
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
|
||||
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.restored')));
|
||||
}
|
||||
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);
|
||||
|
||||
|
||||
$id = $userId;
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))), 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ class AssetMaintenancesController extends Controller
|
||||
$assetMaintenance = new AssetMaintenance();
|
||||
$assetMaintenance->supplier_id = $request->input('supplier_id');
|
||||
$assetMaintenance->is_warranty = $request->input('is_warranty');
|
||||
$assetMaintenance->cost = $request->input('cost');
|
||||
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
|
||||
$assetMaintenance->notes = $request->input('notes');
|
||||
$asset = Asset::find($request->input('asset_id'));
|
||||
|
||||
@@ -148,20 +148,30 @@ class AssetMaintenancesController extends Controller
|
||||
*/
|
||||
public function edit($assetMaintenanceId = null)
|
||||
{
|
||||
$this->authorize('update', Asset::class);
|
||||
// Check if the asset maintenance exists
|
||||
$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 ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
|
||||
// Redirect to the asset maintenance management page
|
||||
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
|
||||
// Redirect to the improvement management page
|
||||
return redirect()->route('maintenances.index')
|
||||
->with('error', trans('admin/asset_maintenances/message.not_found'));
|
||||
} elseif (! $assetMaintenance->asset) {
|
||||
return redirect()->route('maintenances.index')
|
||||
->with('error', 'The asset associated with this maintenance does not exist.');
|
||||
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
|
||||
return static::getInsufficientPermissionsRedirect();
|
||||
}
|
||||
|
||||
if ($assetMaintenance->completion_date == '0000-00-00') {
|
||||
$assetMaintenance->completion_date = null;
|
||||
}
|
||||
|
||||
if ($assetMaintenance->start_date == '0000-00-00') {
|
||||
$assetMaintenance->start_date = null;
|
||||
}
|
||||
|
||||
if ($assetMaintenance->cost == '0.00') {
|
||||
$assetMaintenance->cost = null;
|
||||
}
|
||||
|
||||
// Prepare Improvement Type List
|
||||
$assetMaintenanceType = [
|
||||
@@ -193,17 +203,15 @@ class AssetMaintenancesController extends Controller
|
||||
// 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 ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
|
||||
// Redirect to the asset maintenance management page
|
||||
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
|
||||
return redirect()->route('maintenances.index')
|
||||
->with('error', trans('admin/asset_maintenances/message.not_found'));
|
||||
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
|
||||
return static::getInsufficientPermissionsRedirect();
|
||||
}
|
||||
|
||||
$assetMaintenance->supplier_id = $request->input('supplier_id');
|
||||
$assetMaintenance->is_warranty = $request->input('is_warranty');
|
||||
$assetMaintenance->cost = $request->input('cost');
|
||||
$assetMaintenance->cost = Helper::ParseCurrency($request->input('cost'));
|
||||
$assetMaintenance->notes = $request->input('notes');
|
||||
|
||||
$asset = Asset::find(request('asset_id'));
|
||||
|
||||
@@ -4,16 +4,10 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\CustomField;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Input;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Redirect;
|
||||
use Request;
|
||||
use Storage;
|
||||
@@ -81,15 +75,14 @@ class AssetModelsController extends Controller
|
||||
$model->depreciation_id = $request->input('depreciation_id');
|
||||
$model->name = $request->input('name');
|
||||
$model->model_number = $request->input('model_number');
|
||||
$model->min_amt = $request->input('min_amt');
|
||||
$model->manufacturer_id = $request->input('manufacturer_id');
|
||||
$model->category_id = $request->input('category_id');
|
||||
$model->notes = $request->input('notes');
|
||||
$model->user_id = Auth::id();
|
||||
$model->requestable = Request::has('requestable');
|
||||
|
||||
if ($request->input('fieldset_id') != '') {
|
||||
$model->fieldset_id = $request->input('fieldset_id');
|
||||
if ($request->input('custom_fieldset') != '') {
|
||||
$model->fieldset_id = e($request->input('custom_fieldset'));
|
||||
}
|
||||
|
||||
$model = $request->handleImages($model);
|
||||
@@ -97,11 +90,10 @@ class AssetModelsController extends Controller
|
||||
// Was it created?
|
||||
if ($model->save()) {
|
||||
if ($this->shouldAddDefaultValues($request->input())) {
|
||||
if (!$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'))){
|
||||
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.fieldset_default_value.error'));
|
||||
}
|
||||
$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'));
|
||||
}
|
||||
|
||||
// Redirect to the new model page
|
||||
return redirect()->route('models.index')->with('success', trans('admin/models/message.create.success'));
|
||||
}
|
||||
|
||||
@@ -158,7 +150,6 @@ class AssetModelsController extends Controller
|
||||
$model->eol = $request->input('eol');
|
||||
$model->name = $request->input('name');
|
||||
$model->model_number = $request->input('model_number');
|
||||
$model->min_amt = $request->input('min_amt');
|
||||
$model->manufacturer_id = $request->input('manufacturer_id');
|
||||
$model->category_id = $request->input('category_id');
|
||||
$model->notes = $request->input('notes');
|
||||
@@ -166,28 +157,17 @@ class AssetModelsController extends Controller
|
||||
|
||||
$this->removeCustomFieldsDefaultValues($model);
|
||||
|
||||
$model->fieldset_id = $request->input('fieldset_id');
|
||||
if ($request->input('custom_fieldset') == '') {
|
||||
$model->fieldset_id = null;
|
||||
} else {
|
||||
$model->fieldset_id = $request->input('custom_fieldset');
|
||||
|
||||
if ($this->shouldAddDefaultValues($request->input())) {
|
||||
if (!$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'))){
|
||||
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.fieldset_default_value.error'));
|
||||
if ($this->shouldAddDefaultValues($request->input())) {
|
||||
$this->assignCustomFieldsDefaultValues($model, $request->input('default_values'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if ($model->save()) {
|
||||
if ($model->wasChanged('eol')) {
|
||||
if ($model->eol > 0) {
|
||||
$newEol = $model->eol;
|
||||
$model->assets()->whereNotNull('purchase_date')->where('eol_explicit', false)
|
||||
->update(['asset_eol_date' => DB::raw('DATE_ADD(purchase_date, INTERVAL ' . $newEol . ' MONTH)')]);
|
||||
} elseif ($model->eol == 0) {
|
||||
$model->assets()->whereNotNull('purchase_date')->where('eol_explicit', false)
|
||||
->update(['asset_eol_date' => DB::raw('null')]);
|
||||
}
|
||||
}
|
||||
return redirect()->route('models.index')->with('success', trans('admin/models/message.update.success'));
|
||||
}
|
||||
|
||||
@@ -209,7 +189,7 @@ class AssetModelsController extends Controller
|
||||
$this->authorize('delete', AssetModel::class);
|
||||
// Check if the model exists
|
||||
if (is_null($model = AssetModel::find($modelId))) {
|
||||
return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
|
||||
return redirect()->route('models.index')->with('error', trans('admin/models/message.not_found'));
|
||||
}
|
||||
|
||||
if ($model->assets()->count() > 0) {
|
||||
@@ -237,42 +217,22 @@ class AssetModelsController extends Controller
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @since [v1.0]
|
||||
* @param int $id
|
||||
* @param int $modelId
|
||||
* @return Redirect
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function getRestore($id)
|
||||
public function getRestore($modelId = null)
|
||||
{
|
||||
$this->authorize('create', AssetModel::class);
|
||||
// Get user information
|
||||
$model = AssetModel::withTrashed()->find($modelId);
|
||||
|
||||
if ($model = AssetModel::withTrashed()->find($id)) {
|
||||
if (isset($model->id)) {
|
||||
$model->restore();
|
||||
|
||||
if ($model->deleted_at == '') {
|
||||
return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.asset_model')]));
|
||||
}
|
||||
|
||||
if ($model->restore()) {
|
||||
$logaction = new Actionlog();
|
||||
$logaction->item_type = User::class;
|
||||
$logaction->item_id = $model->id;
|
||||
$logaction->created_at = date('Y-m-d H:i:s');
|
||||
$logaction->user_id = Auth::user()->id;
|
||||
$logaction->logaction('restore');
|
||||
|
||||
|
||||
// Redirect them to the deleted page if there are more, otherwise the section index
|
||||
$deleted_models = AssetModel::onlyTrashed()->count();
|
||||
if ($deleted_models > 0) {
|
||||
return redirect()->back()->with('success', trans('admin/models/message.restore.success'));
|
||||
}
|
||||
return redirect()->route('models.index')->with('success', trans('admin/models/message.restore.success'));
|
||||
}
|
||||
|
||||
// Check validation
|
||||
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.asset_model'), 'error' => $model->getErrors()->first()]));
|
||||
return redirect()->route('models.index')->with('success', trans('admin/models/message.restore.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
|
||||
return redirect()->back()->with('error', trans('admin/models/message.not_found'));
|
||||
|
||||
}
|
||||
|
||||
@@ -289,7 +249,7 @@ class AssetModelsController extends Controller
|
||||
public function show($modelId = null)
|
||||
{
|
||||
$this->authorize('view', AssetModel::class);
|
||||
$model = AssetModel::withTrashed()->withCount('assets')->find($modelId);
|
||||
$model = AssetModel::withTrashed()->find($modelId);
|
||||
|
||||
if (isset($model->id)) {
|
||||
return view('models/view', compact('model'));
|
||||
@@ -321,7 +281,6 @@ class AssetModelsController extends Controller
|
||||
return view('models/edit')
|
||||
->with('depreciation_list', Helper::depreciationList())
|
||||
->with('item', $model)
|
||||
->with('model_id', $model_to_clone->id)
|
||||
->with('clone_model', $model_to_clone);
|
||||
}
|
||||
|
||||
@@ -443,6 +402,7 @@ class AssetModelsController extends Controller
|
||||
$del_count = 0;
|
||||
|
||||
foreach ($models as $model) {
|
||||
\Log::debug($model->id);
|
||||
|
||||
if ($model->assets_count > 0) {
|
||||
$del_error_count++;
|
||||
@@ -452,6 +412,8 @@ class AssetModelsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
\Log::debug($del_count);
|
||||
\Log::debug($del_error_count);
|
||||
|
||||
if ($del_error_count == 0) {
|
||||
return redirect()->route('models.index')
|
||||
@@ -477,7 +439,7 @@ class AssetModelsController extends Controller
|
||||
{
|
||||
return ! empty($input['add_default_values'])
|
||||
&& ! empty($input['default_values'])
|
||||
&& ! empty($input['fieldset_id']);
|
||||
&& ! empty($input['custom_fieldset']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -487,34 +449,8 @@ class AssetModelsController extends Controller
|
||||
* @param array $defaultValues
|
||||
* @return void
|
||||
*/
|
||||
private function assignCustomFieldsDefaultValues(AssetModel $model, array $defaultValues): bool
|
||||
private function assignCustomFieldsDefaultValues(AssetModel $model, array $defaultValues)
|
||||
{
|
||||
$data = array();
|
||||
foreach ($defaultValues as $customFieldId => $defaultValue) {
|
||||
$customField = CustomField::find($customFieldId);
|
||||
|
||||
$data[$customField->db_column] = $defaultValue;
|
||||
}
|
||||
|
||||
$fieldsets = $model->fieldset->validation_rules();
|
||||
$rules = array();
|
||||
|
||||
foreach ($fieldsets as $fieldset => $validation){
|
||||
// If the field is marked as required, eliminate the rule so it doesn't interfere with the default values
|
||||
// (we are at model level, the rule still applies when creating a new asset using this model)
|
||||
$index = array_search('required', $validation);
|
||||
if ($index !== false){
|
||||
$validation[$index] = 'nullable';
|
||||
}
|
||||
$rules[$fieldset] = $validation;
|
||||
}
|
||||
|
||||
$validator = Validator::make($data, $rules);
|
||||
|
||||
if($validator->fails()){
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($defaultValues as $customFieldId => $defaultValue) {
|
||||
if(is_array($defaultValue)){
|
||||
$model->defaultValues()->attach($customFieldId, ['default_value' => implode(', ', $defaultValue)]);
|
||||
@@ -522,7 +458,6 @@ class AssetModelsController extends Controller
|
||||
$model->defaultValues()->attach($customFieldId, ['default_value' => $defaultValue]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\AssetModel;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AssetModelsFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Upload a file to the server.
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
* @param int $modelId
|
||||
* @return Redirect
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*@since [v1.0]
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $modelId = null)
|
||||
{
|
||||
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, e($request->get('notes')));
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', trans('general.file_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 $modelId
|
||||
* @param int $fileId
|
||||
* @since [v1.0]
|
||||
* @return View
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function show($modelId = null, $fileId = null)
|
||||
{
|
||||
$model = AssetModel::find($modelId);
|
||||
// the asset is valid
|
||||
if (isset($model->id)) {
|
||||
$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;
|
||||
\Log::debug('Checking for '.$file);
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
// Prepare the error message
|
||||
$error = trans('admin/hardware/message.does_not_exist', ['id' => $fileId]);
|
||||
|
||||
// Redirect to the hardware management page
|
||||
return redirect()->route('hardware.index')->with('error', $error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the associated file
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $modelId
|
||||
* @param int $fileId
|
||||
* @since [v1.0]
|
||||
* @return View
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function destroy($modelId = null, $fileId = null)
|
||||
{
|
||||
$model = AssetModel::find($modelId);
|
||||
$this->authorize('update', $model);
|
||||
$rel_path = 'private_uploads/assetmodels';
|
||||
|
||||
// the asset is valid
|
||||
if (isset($model->id)) {
|
||||
$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()->with('success', trans('admin/hardware/message.deletefile.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('success', trans('admin/hardware/message.deletefile.success'));
|
||||
}
|
||||
|
||||
// Redirect to the hardware management page
|
||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,8 @@ use App\Events\CheckoutableCheckedIn;
|
||||
use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AssetCheckinRequest;
|
||||
use App\Http\Traits\MigratesLegacyAssetLocations;
|
||||
use App\Models\Asset;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\LicenseSeat;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
@@ -17,8 +15,6 @@ use Illuminate\Support\Facades\View;
|
||||
|
||||
class AssetCheckinController extends Controller
|
||||
{
|
||||
use MigratesLegacyAssetLocations;
|
||||
|
||||
/**
|
||||
* Returns a view that presents a form to check an asset back into inventory.
|
||||
*
|
||||
@@ -71,9 +67,10 @@ class AssetCheckinController extends Controller
|
||||
}
|
||||
|
||||
$asset->expected_checkin = null;
|
||||
//$asset->last_checkout = null;
|
||||
$asset->last_checkin = now();
|
||||
$asset->last_checkout = null;
|
||||
$asset->assigned_to = null;
|
||||
$asset->assignedTo()->disassociate($asset);
|
||||
$asset->assigned_type = null;
|
||||
$asset->accepted = null;
|
||||
$asset->name = $request->get('name');
|
||||
|
||||
@@ -81,31 +78,39 @@ class AssetCheckinController extends Controller
|
||||
$asset->status_id = e($request->get('status_id'));
|
||||
}
|
||||
|
||||
$this->migrateLegacyLocations($asset);
|
||||
// This is just meant to correct legacy issues where some user data would have 0
|
||||
// as a location ID, which isn't valid. Later versions of Snipe-IT have stricter validation
|
||||
// rules, so it's necessary to fix this for long-time users. It's kinda gross, but will help
|
||||
// people (and their data) in the long run
|
||||
|
||||
if ($asset->rtd_location_id == '0') {
|
||||
\Log::debug('Manually override the RTD location IDs');
|
||||
\Log::debug('Original RTD Location ID: '.$asset->rtd_location_id);
|
||||
$asset->rtd_location_id = '';
|
||||
\Log::debug('New RTD Location ID: '.$asset->rtd_location_id);
|
||||
}
|
||||
|
||||
if ($asset->location_id == '0') {
|
||||
\Log::debug('Manually override the location IDs');
|
||||
\Log::debug('Original Location ID: '.$asset->location_id);
|
||||
$asset->location_id = '';
|
||||
\Log::debug('New RTD Location ID: '.$asset->location_id);
|
||||
}
|
||||
|
||||
$asset->location_id = $asset->rtd_location_id;
|
||||
\Log::debug('After Location ID: '.$asset->location_id);
|
||||
\Log::debug('After RTD Location ID: '.$asset->rtd_location_id);
|
||||
|
||||
if ($request->filled('location_id')) {
|
||||
\Log::debug('NEW Location ID: '.$request->get('location_id'));
|
||||
$asset->location_id = $request->get('location_id');
|
||||
|
||||
if ($request->get('update_default_location') == 0){
|
||||
$asset->rtd_location_id = $request->get('location_id');
|
||||
}
|
||||
$asset->location_id = e($request->get('location_id'));
|
||||
}
|
||||
|
||||
$originalValues = $asset->getRawOriginal();
|
||||
|
||||
$checkin_at = date('Y-m-d H:i:s');
|
||||
if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) {
|
||||
$originalValues['action_date'] = $checkin_at;
|
||||
$checkin_at = $request->get('checkin_at');
|
||||
$checkin_at = date('Y-m-d');
|
||||
if ($request->filled('checkin_at')) {
|
||||
$checkin_at = $request->input('checkin_at');
|
||||
}
|
||||
|
||||
$asset->licenseseats->each(function (LicenseSeat $seat) {
|
||||
$seat->update(['assigned_to' => null]);
|
||||
});
|
||||
|
||||
// Get all pending Acceptances for this asset and delete them
|
||||
$acceptances = CheckoutAcceptance::pending()->whereHasMorph('checkoutable',
|
||||
[Asset::class],
|
||||
@@ -118,7 +123,7 @@ class AssetCheckinController extends Controller
|
||||
|
||||
// Was the asset updated?
|
||||
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));
|
||||
|
||||
if ((isset($user)) && ($backto == 'user')) {
|
||||
return redirect()->route('users.show', $user->id)->with('success', trans('admin/hardware/message.checkin.success'));
|
||||
|
||||
@@ -27,7 +27,7 @@ class AssetCheckoutController extends Controller
|
||||
public function create($assetId)
|
||||
{
|
||||
// Check if the asset exists
|
||||
if (is_null($asset = Asset::with('company')->find(e($assetId)))) {
|
||||
if (is_null($asset = Asset::find(e($assetId)))) {
|
||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||
}
|
||||
|
||||
@@ -80,25 +80,7 @@ class AssetCheckoutController extends Controller
|
||||
$asset->status_id = $request->get('status_id');
|
||||
}
|
||||
|
||||
if(!empty($asset->licenseseats->all())){
|
||||
if(request('checkout_to_type') == 'user') {
|
||||
foreach ($asset->licenseseats as $seat){
|
||||
$seat->assigned_to = $target->id;
|
||||
$seat->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$settings = \App\Models\Setting::getSettings();
|
||||
|
||||
// We have to check whether $target->company_id is null here since locations don't have a company yet
|
||||
if (($settings->full_multiple_companies_support) && ((!is_null($target->company_id)) && (!is_null($asset->company_id)))) {
|
||||
if ($target->company_id != $asset->company_id){
|
||||
return redirect()->to("hardware/$assetId/checkout")->with('error', trans('general.error_user_company'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->get('note'), $request->get('name'))) {
|
||||
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), $request->get('name'))) {
|
||||
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.checkout.success'));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,25 +4,26 @@ namespace App\Http\Controllers\Assets;
|
||||
|
||||
use App\Helpers\StorageHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UploadFileRequest;
|
||||
use App\Http\Requests\AssetFileRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use enshrined\svgSanitize\Sanitizer;
|
||||
|
||||
class AssetFilesController extends Controller
|
||||
{
|
||||
/**
|
||||
* Upload a file to the server.
|
||||
*
|
||||
* @param UploadFileRequest $request
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param AssetFileRequest $request
|
||||
* @param int $assetId
|
||||
* @return Redirect
|
||||
* @since [v1.0]
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*@since [v1.0]
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
*/
|
||||
public function store(UploadFileRequest $request, $assetId = null)
|
||||
public function store(AssetFileRequest $request, $assetId = null)
|
||||
{
|
||||
if (! $asset = Asset::find($assetId)) {
|
||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||
@@ -36,7 +37,28 @@ class AssetFilesController extends Controller
|
||||
}
|
||||
|
||||
foreach ($request->file('file') as $file) {
|
||||
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
|
||||
|
||||
$extension = $file->getClientOriginalExtension();
|
||||
$file_name = 'hardware-'.$asset->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
|
||||
|
||||
// Check for SVG and sanitize it
|
||||
if ($extension=='svg') {
|
||||
\Log::debug('This is an SVG');
|
||||
|
||||
$sanitizer = new Sanitizer();
|
||||
$dirtySVG = file_get_contents($file->getRealPath());
|
||||
$cleanSVG = $sanitizer->sanitize($dirtySVG);
|
||||
|
||||
try {
|
||||
Storage::put('private_uploads/assets/'.$file_name, $cleanSVG);
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug('Upload no workie :( ');
|
||||
\Log::debug($e);
|
||||
}
|
||||
} else {
|
||||
Storage::put('private_uploads/assets/'.$file_name, file_get_contents($file));
|
||||
}
|
||||
|
||||
|
||||
$asset->logUpload($file_name, e($request->get('notes')));
|
||||
}
|
||||
@@ -57,14 +79,14 @@ class AssetFilesController extends Controller
|
||||
* @return View
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function show($assetId = null, $fileId = null)
|
||||
public function show($assetId = null, $fileId = null, $download = true)
|
||||
{
|
||||
$asset = Asset::find($assetId);
|
||||
// the asset is valid
|
||||
if (isset($asset->id)) {
|
||||
$this->authorize('view', $asset);
|
||||
|
||||
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
|
||||
if (! $log = Actionlog::find($fileId)) {
|
||||
return response('No matching record for that asset/file', 500)
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
@@ -81,13 +103,12 @@ class AssetFilesController extends Controller
|
||||
->header('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
if (request('inline') == 'true') {
|
||||
if ($download != 'true') {
|
||||
if ($contents = file_get_contents(Storage::url($file))) {
|
||||
return Response::make(Storage::url($file)->header('Content-Type', mime_content_type($file)));
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Content-Disposition' => 'inline',
|
||||
];
|
||||
|
||||
return Storage::download($file, $log->filename, $headers);
|
||||
return JsonResponse::create(['error' => 'Failed validation: '], 500);
|
||||
}
|
||||
|
||||
return StorageHelper::downloader($file);
|
||||
|
||||
@@ -6,28 +6,31 @@ use App\Helpers\Helper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ImageUploadRequest;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Manufacturer;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\CheckoutRequest;
|
||||
use App\Models\Company;
|
||||
use App\Models\Location;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Statuslabel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\View\Label;
|
||||
use Auth;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use DB;
|
||||
use Gate;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Input;
|
||||
use Intervention\Image\Facades\Image;
|
||||
use League\Csv\Reader;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
use League\Csv\Statement;
|
||||
use Paginator;
|
||||
use Redirect;
|
||||
use Response;
|
||||
use Slack;
|
||||
use Str;
|
||||
use TCPDF;
|
||||
use View;
|
||||
|
||||
/**
|
||||
* This class controls all actions related to assets for
|
||||
@@ -102,10 +105,6 @@ class AssetsController extends Controller
|
||||
{
|
||||
$this->authorize(Asset::class);
|
||||
|
||||
// There are a lot more rules to add here but prevents
|
||||
// errors around `asset_tags` not being present below.
|
||||
$this->validate($request, ['asset_tags' => ['required', 'array']]);
|
||||
|
||||
// Handle asset tags - there could be one, or potentially many.
|
||||
// This is only necessary on create, not update, since bulk editing is handled
|
||||
// differently
|
||||
@@ -135,23 +134,23 @@ class AssetsController extends Controller
|
||||
$asset->order_number = $request->input('order_number');
|
||||
$asset->notes = $request->input('notes');
|
||||
$asset->user_id = Auth::id();
|
||||
$asset->status_id = request('status_id');
|
||||
$asset->archived = '0';
|
||||
$asset->physical = '1';
|
||||
$asset->depreciate = '0';
|
||||
$asset->status_id = request('status_id', 0);
|
||||
$asset->warranty_months = request('warranty_months', null);
|
||||
$asset->purchase_cost = request('purchase_cost');
|
||||
$asset->purchase_cost = Helper::ParseCurrency($request->get('purchase_cost'));
|
||||
$asset->purchase_date = request('purchase_date', null);
|
||||
$asset->asset_eol_date = request('asset_eol_date', null);
|
||||
$asset->assigned_to = request('assigned_to', null);
|
||||
$asset->supplier_id = request('supplier_id', null);
|
||||
$asset->supplier_id = request('supplier_id', 0);
|
||||
$asset->requestable = request('requestable', 0);
|
||||
$asset->rtd_location_id = request('rtd_location_id', null);
|
||||
$asset->byod = request('byod', 0);
|
||||
|
||||
if (! empty($settings->audit_interval)) {
|
||||
$asset->next_audit_date = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
|
||||
}
|
||||
|
||||
// Set location_id to rtd_location_id ONLY if the asset isn't being checked out
|
||||
if (!request('assigned_user') && !request('assigned_asset') && !request('assigned_location')) {
|
||||
if ($asset->assigned_to == '') {
|
||||
$asset->location_id = $request->input('rtd_location_id', null);
|
||||
}
|
||||
|
||||
@@ -168,17 +167,17 @@ class AssetsController extends Controller
|
||||
foreach ($model->fieldset->fields as $field) {
|
||||
if ($field->field_encrypted == '1') {
|
||||
if (Gate::allows('admin')) {
|
||||
if (is_array($request->input($field->db_column))) {
|
||||
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
|
||||
if (is_array($request->input($field->convertUnicodeDbSlug()))) {
|
||||
$asset->{$field->convertUnicodeDbSlug()} = \Crypt::encrypt(e(implode(', ', $request->input($field->convertUnicodeDbSlug()))));
|
||||
} else {
|
||||
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
|
||||
$asset->{$field->convertUnicodeDbSlug()} = \Crypt::encrypt(e($request->input($field->convertUnicodeDbSlug())));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (is_array($request->input($field->db_column))) {
|
||||
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
|
||||
if (is_array($request->input($field->convertUnicodeDbSlug()))) {
|
||||
$asset->{$field->convertUnicodeDbSlug()} = implode(', ', $request->input($field->convertUnicodeDbSlug()));
|
||||
} else {
|
||||
$asset->{$field->db_column} = $request->input($field->db_column);
|
||||
$asset->{$field->convertUnicodeDbSlug()} = $request->input($field->convertUnicodeDbSlug());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,27 +201,18 @@ class AssetsController extends Controller
|
||||
}
|
||||
|
||||
$success = true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
\Log::debug(e($asset->asset_tag));
|
||||
// Redirect to the asset listing page
|
||||
return redirect()->route('hardware.index')
|
||||
->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset->id), 'id', 'tag' => e($asset->asset_tag)]));
|
||||
|
||||
|
||||
->with('success', trans('admin/hardware/message.create.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->withErrors($asset->getErrors());
|
||||
}
|
||||
|
||||
public function getOptionCookie(Request $request){
|
||||
$value = $request->cookie('optional_info');
|
||||
echo $value;
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a view that presents a form to edit an existing asset.
|
||||
*
|
||||
@@ -291,10 +281,10 @@ class AssetsController extends Controller
|
||||
/**
|
||||
* Validate and process asset edit form.
|
||||
*
|
||||
* @param int $assetId
|
||||
* @return \Illuminate\Http\RedirectResponse|Redirect
|
||||
*@since [v1.0]
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $assetId
|
||||
* @since [v1.0]
|
||||
* @return Redirect
|
||||
*/
|
||||
public function update(ImageUploadRequest $request, $assetId = null)
|
||||
{
|
||||
@@ -307,41 +297,14 @@ class AssetsController extends Controller
|
||||
|
||||
$asset->status_id = $request->input('status_id', null);
|
||||
$asset->warranty_months = $request->input('warranty_months', null);
|
||||
$asset->purchase_cost = $request->input('purchase_cost', null);
|
||||
$asset->purchase_date = $request->input('purchase_date', null);
|
||||
if ($request->filled('purchase_date') && !$request->filled('asset_eol_date') && ($asset->model->eol > 0)) {
|
||||
$asset->purchase_date = $request->input('purchase_date', null);
|
||||
$asset->asset_eol_date = Carbon::parse($request->input('purchase_date'))->addMonths($asset->model->eol)->format('Y-m-d');
|
||||
$asset->eol_explicit = false;
|
||||
} elseif ($request->filled('asset_eol_date')) {
|
||||
$asset->asset_eol_date = $request->input('asset_eol_date', null);
|
||||
$months = Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date);
|
||||
if($asset->model->eol) {
|
||||
if($months != $asset->model->eol > 0) {
|
||||
$asset->eol_explicit = true;
|
||||
} else {
|
||||
$asset->eol_explicit = false;
|
||||
}
|
||||
} else {
|
||||
$asset->eol_explicit = true;
|
||||
}
|
||||
} elseif (!$request->filled('asset_eol_date') && (($asset->model->eol) == 0)) {
|
||||
$asset->asset_eol_date = null;
|
||||
$asset->eol_explicit = false;
|
||||
}
|
||||
$asset->purchase_cost = Helper::ParseCurrency($request->input('purchase_cost', null));
|
||||
$asset->purchase_date = $request->input('purchase_date', null);
|
||||
$asset->supplier_id = $request->input('supplier_id', null);
|
||||
$asset->expected_checkin = $request->input('expected_checkin', null);
|
||||
|
||||
// If the box isn't checked, it's not in the request at all.
|
||||
$asset->requestable = $request->filled('requestable');
|
||||
$asset->rtd_location_id = $request->input('rtd_location_id', null);
|
||||
$asset->byod = $request->input('byod', 0);
|
||||
|
||||
$status = Statuslabel::find($asset->status_id);
|
||||
|
||||
if($status->archived){
|
||||
$asset->assigned_to = null;
|
||||
}
|
||||
|
||||
if ($asset->assigned_to == '') {
|
||||
$asset->location_id = $request->input('rtd_location_id', null);
|
||||
@@ -353,7 +316,7 @@ class AssetsController extends Controller
|
||||
unlink(public_path().'/uploads/assets/'.$asset->image);
|
||||
$asset->image = '';
|
||||
} catch (\Exception $e) {
|
||||
Log::info($e);
|
||||
\Log::info($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +330,7 @@ class AssetsController extends Controller
|
||||
$asset->order_number = $request->input('order_number');
|
||||
$asset->asset_tag = $asset_tag[1];
|
||||
$asset->notes = $request->input('notes');
|
||||
$asset->physical = '1';
|
||||
|
||||
$asset = $request->handleImages($asset);
|
||||
|
||||
@@ -379,17 +343,17 @@ class AssetsController extends Controller
|
||||
foreach ($model->fieldset->fields as $field) {
|
||||
if ($field->field_encrypted == '1') {
|
||||
if (Gate::allows('admin')) {
|
||||
if (is_array($request->input($field->db_column))) {
|
||||
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
|
||||
if (is_array($request->input($field->convertUnicodeDbSlug()))) {
|
||||
$asset->{$field->convertUnicodeDbSlug()} = \Crypt::encrypt(e(implode(', ', $request->input($field->convertUnicodeDbSlug()))));
|
||||
} else {
|
||||
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
|
||||
$asset->{$field->convertUnicodeDbSlug()} = \Crypt::encrypt(e($request->input($field->convertUnicodeDbSlug())));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (is_array($request->input($field->db_column))) {
|
||||
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
|
||||
if (is_array($request->input($field->convertUnicodeDbSlug()))) {
|
||||
$asset->{$field->convertUnicodeDbSlug()} = implode(', ', $request->input($field->convertUnicodeDbSlug()));
|
||||
} else {
|
||||
$asset->{$field->db_column} = $request->input($field->db_column);
|
||||
$asset->{$field->convertUnicodeDbSlug()} = $request->input($field->convertUnicodeDbSlug());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -430,7 +394,7 @@ class AssetsController extends Controller
|
||||
try {
|
||||
Storage::disk('public')->delete('assets'.'/'.$asset->image);
|
||||
} catch (\Exception $e) {
|
||||
Log::debug($e);
|
||||
\Log::debug($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,12 +428,11 @@ class AssetsController extends Controller
|
||||
* @since [v3.0]
|
||||
* @return Redirect
|
||||
*/
|
||||
public function getAssetByTag(Request $request, $tag=null)
|
||||
public function getAssetByTag(Request $request)
|
||||
{
|
||||
$tag = $tag ? $tag : $request->get('assetTag');
|
||||
$topsearch = ($request->get('topsearch') == 'true');
|
||||
|
||||
if (! $asset = Asset::where('asset_tag', '=', $tag)->first()) {
|
||||
if (! $asset = Asset::where('asset_tag', '=', $request->get('assetTag'))->first()) {
|
||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||
}
|
||||
$this->authorize('view', $asset);
|
||||
@@ -526,33 +489,31 @@ class AssetsController extends Controller
|
||||
public function getBarCode($assetId = null)
|
||||
{
|
||||
$settings = Setting::getSettings();
|
||||
if ($asset = Asset::withTrashed()->find($assetId)) {
|
||||
$barcode_file = public_path().'/uploads/barcodes/'.str_slug($settings->alt_barcode).'-'.str_slug($asset->asset_tag).'.png';
|
||||
$asset = Asset::find($assetId);
|
||||
$barcode_file = public_path().'/uploads/barcodes/'.str_slug($settings->alt_barcode).'-'.str_slug($asset->asset_tag).'.png';
|
||||
|
||||
if (isset($asset->id, $asset->asset_tag)) {
|
||||
if (file_exists($barcode_file)) {
|
||||
$header = ['Content-type' => 'image/png'];
|
||||
if (isset($asset->id, $asset->asset_tag)) {
|
||||
if (file_exists($barcode_file)) {
|
||||
$header = ['Content-type' => 'image/png'];
|
||||
|
||||
return response()->file($barcode_file, $header);
|
||||
} else {
|
||||
// Calculate barcode width in pixel based on label width (inch)
|
||||
$barcode_width = ($settings->labels_width - $settings->labels_display_sgutter) * 200.000000000001;
|
||||
return response()->file($barcode_file, $header);
|
||||
} else {
|
||||
// Calculate barcode width in pixel based on label width (inch)
|
||||
$barcode_width = ($settings->labels_width - $settings->labels_display_sgutter) * 200.000000000001;
|
||||
|
||||
$barcode = new \Com\Tecnick\Barcode\Barcode();
|
||||
try {
|
||||
$barcode_obj = $barcode->getBarcodeObj($settings->alt_barcode, $asset->asset_tag, ($barcode_width < 300 ? $barcode_width : 300), 50);
|
||||
file_put_contents($barcode_file, $barcode_obj->getPngData());
|
||||
$barcode = new \Com\Tecnick\Barcode\Barcode();
|
||||
try {
|
||||
$barcode_obj = $barcode->getBarcodeObj($settings->alt_barcode, $asset->asset_tag, ($barcode_width < 300 ? $barcode_width : 300), 50);
|
||||
file_put_contents($barcode_file, $barcode_obj->getPngData());
|
||||
|
||||
return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
|
||||
} catch (\Exception $e) {
|
||||
Log::debug('The barcode format is invalid.');
|
||||
return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug('The barcode format is invalid.');
|
||||
|
||||
return response(file_get_contents(public_path('uploads/barcodes/invalid_barcode.gif')))->header('Content-type', 'image/gif');
|
||||
}
|
||||
return response(file_get_contents(public_path('uploads/barcodes/invalid_barcode.gif')))->header('Content-type', 'image/gif');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -568,11 +529,9 @@ class AssetsController extends Controller
|
||||
$asset = Asset::find($assetId);
|
||||
$this->authorize('view', $asset);
|
||||
|
||||
return (new Label())
|
||||
->with('assets', collect([ $asset ]))
|
||||
return view('hardware/labels')
|
||||
->with('assets', Asset::find($asset))
|
||||
->with('settings', Setting::getSettings())
|
||||
->with('template', request()->get('template'))
|
||||
->with('offset', request()->get('offset'))
|
||||
->with('bulkedit', false)
|
||||
->with('count', 0);
|
||||
}
|
||||
@@ -650,11 +609,7 @@ class AssetsController extends Controller
|
||||
$csv->setHeaderOffset(0);
|
||||
$header = $csv->getHeader();
|
||||
$isCheckinHeaderExplicit = in_array('checkin date', (array_map('strtolower', $header)));
|
||||
try {
|
||||
$results = $csv->getRecords();
|
||||
} catch (\Exception $e) {
|
||||
return back()->with('error', trans('general.error_in_import_file', ['error' => $e->getMessage()]));
|
||||
}
|
||||
$results = $csv->getRecords();
|
||||
$item = [];
|
||||
$status = [];
|
||||
$status['error'] = [];
|
||||
@@ -741,11 +696,11 @@ class AssetsController extends Controller
|
||||
|
||||
if ($isCheckinHeaderExplicit) {
|
||||
|
||||
// if checkin date header exists, assume that empty or future date is still checked out
|
||||
// if checkin is before today's date, assume it's checked in and do not assign user ID, if checkin date is in the future or blank, this is the expected checkin date, items are checked out
|
||||
//if checkin date header exists, assume that empty or future date is still checked out
|
||||
//if checkin is before todays date, assume it's checked in and do not assign user ID, if checkin date is in the future or blank, this is the expected checkin date, items is checked out
|
||||
|
||||
if ((strtotime($checkin_date) > strtotime(Carbon::now())) || (empty($checkin_date)))
|
||||
{
|
||||
if ((strtotime($checkin_date) > strtotime(Carbon::now())) || (empty($checkin_date))
|
||||
) {
|
||||
//only do this if item is checked out
|
||||
$asset->assigned_to = $user->id;
|
||||
$asset->assigned_type = User::class;
|
||||
@@ -790,7 +745,7 @@ class AssetsController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a deleted asset.
|
||||
* Retore a deleted asset.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @param int $assetId
|
||||
@@ -799,24 +754,21 @@ class AssetsController extends Controller
|
||||
*/
|
||||
public function getRestore($assetId = null)
|
||||
{
|
||||
if ($asset = Asset::withTrashed()->find($assetId)) {
|
||||
$this->authorize('delete', $asset);
|
||||
// Get asset information
|
||||
$asset = Asset::withTrashed()->find($assetId);
|
||||
$this->authorize('delete', $asset);
|
||||
if (isset($asset->id)) {
|
||||
// Restore the asset
|
||||
Asset::withTrashed()->where('id', $assetId)->restore();
|
||||
|
||||
if ($asset->deleted_at == '') {
|
||||
return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.asset')]));
|
||||
}
|
||||
$logaction = new Actionlog();
|
||||
$logaction->item_type = Asset::class;
|
||||
$logaction->item_id = $asset->id;
|
||||
$logaction->created_at = date('Y-m-d H:i:s');
|
||||
$logaction->user_id = Auth::user()->id;
|
||||
$logaction->logaction('restored');
|
||||
|
||||
if ($asset->restore()) {
|
||||
// Redirect them to the deleted page if there are more, otherwise the section index
|
||||
$deleted_assets = Asset::onlyTrashed()->count();
|
||||
if ($deleted_assets > 0) {
|
||||
return redirect()->back()->with('success', trans('admin/hardware/message.restore.success'));
|
||||
}
|
||||
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success'));
|
||||
}
|
||||
|
||||
// Check validation to make sure we're not restoring an asset with the same asset tag (or unique attribute) as an existing asset
|
||||
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.asset'), 'error' => $asset->getErrors()->first()]));
|
||||
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success'));
|
||||
}
|
||||
|
||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
|
||||
@@ -871,7 +823,7 @@ class AssetsController extends Controller
|
||||
'next_audit_date' => 'date|nullable',
|
||||
];
|
||||
|
||||
$validator = Validator::make($request->all(), $rules);
|
||||
$validator = \Validator::make($request->all(), $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()->all()));
|
||||
@@ -888,7 +840,7 @@ class AssetsController extends Controller
|
||||
// Check to see if they checked the box to update the physical location,
|
||||
// not just note it in the audit notes
|
||||
if ($request->input('update_location') == '1') {
|
||||
Log::debug('update location in audit');
|
||||
\Log::debug('update location in audit');
|
||||
$asset->location_id = $request->input('location_id');
|
||||
}
|
||||
|
||||
@@ -909,13 +861,12 @@ class AssetsController extends Controller
|
||||
|
||||
|
||||
$asset->logAudit($request->input('note'), $request->input('location_id'), $file_name);
|
||||
return redirect()->route('assets.audit.due')->with('success', trans('admin/hardware/message.audit.success'));
|
||||
return redirect()->to('hardware')->with('success', trans('admin/hardware/message.audit.success'));
|
||||
}
|
||||
}
|
||||
|
||||
public function getRequestedIndex($user_id = null)
|
||||
{
|
||||
$this->authorize('index', Asset::class);
|
||||
$requestedItems = CheckoutRequest::with('user', 'requestedItem')->whereNull('canceled_at')->with('user', 'requestedItem');
|
||||
|
||||
if ($user_id) {
|
||||
|
||||
@@ -7,17 +7,10 @@ use App\Helpers\Helper;
|
||||
use App\Http\Controllers\CheckInOutRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Statuslabel;
|
||||
use App\Models\Setting;
|
||||
use App\View\Label;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use App\Http\Requests\AssetCheckoutRequest;
|
||||
use App\Models\CustomField;
|
||||
|
||||
class BulkAssetsController extends Controller
|
||||
{
|
||||
@@ -26,13 +19,6 @@ class BulkAssetsController extends Controller
|
||||
/**
|
||||
* Display the bulk edit page.
|
||||
*
|
||||
* This method is super weird because it's kinda of like a controller within a controller.
|
||||
* It's main function is to determine what the bulk action in, and then return a view with
|
||||
* the information that view needs, be it bulk delete, bulk edit, restore, etc.
|
||||
*
|
||||
* This is something that made sense at the time, but sort of doesn't make sense now. A JS front-end to determine form
|
||||
* action would make a lot more sense here and make things a lot more clear.
|
||||
*
|
||||
* @author [A. Gianotto] [<snipe@snipe.net>]
|
||||
* @return View
|
||||
* @internal param int $assetId
|
||||
@@ -41,147 +27,37 @@ class BulkAssetsController extends Controller
|
||||
*/
|
||||
public function edit(Request $request)
|
||||
{
|
||||
$this->authorize('view', Asset::class);
|
||||
$this->authorize('update', Asset::class);
|
||||
|
||||
/**
|
||||
* No asset IDs were passed
|
||||
*/
|
||||
if (! $request->filled('ids')) {
|
||||
return redirect()->back()->with('error', trans('admin/hardware/message.update.no_assets_selected'));
|
||||
return redirect()->back()->with('error', 'No assets selected');
|
||||
}
|
||||
|
||||
$asset_ids = $request->input('ids');
|
||||
|
||||
// Figure out where we need to send the user after the update is complete, and store that in the session
|
||||
$bulk_back_url = request()->headers->get('referer');
|
||||
session(['bulk_back_url' => $bulk_back_url]);
|
||||
|
||||
$allowed_columns = [
|
||||
'id',
|
||||
'name',
|
||||
'asset_tag',
|
||||
'serial',
|
||||
'model_number',
|
||||
'last_checkout',
|
||||
'notes',
|
||||
'expected_checkin',
|
||||
'order_number',
|
||||
'image',
|
||||
'assigned_to',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'purchase_date',
|
||||
'purchase_cost',
|
||||
'last_audit_date',
|
||||
'next_audit_date',
|
||||
'warranty_months',
|
||||
'checkout_counter',
|
||||
'checkin_counter',
|
||||
'requests_counter',
|
||||
'byod',
|
||||
'asset_eol_date',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Make sure the column is allowed, and if it's a custom field, make sure we strip the custom_fields. prefix
|
||||
*/
|
||||
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
|
||||
$sort_override = str_replace('custom_fields.', '', $request->input('sort'));
|
||||
|
||||
// This handles all of the pivot sorting below (versus the assets.* fields in the allowed_columns array)
|
||||
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.id';
|
||||
|
||||
$assets = Asset::with('assignedTo', 'location', 'model')->whereIn('assets.id', $asset_ids);
|
||||
|
||||
$assets = $assets->get();
|
||||
|
||||
if ($assets->isEmpty()) {
|
||||
Log::debug('No assets were found for the provided IDs', ['ids' => $asset_ids]);
|
||||
return redirect()->back()->with('error', trans('admin/hardware/message.update.assets_do_not_exist_or_are_invalid'));
|
||||
}
|
||||
|
||||
$models = $assets->unique('model_id');
|
||||
$modelNames = [];
|
||||
foreach($models as $model) {
|
||||
$modelNames[] = $model->model->name;
|
||||
}
|
||||
$asset_ids = array_values(array_unique($request->input('ids')));
|
||||
|
||||
if ($request->filled('bulk_actions')) {
|
||||
|
||||
|
||||
switch ($request->input('bulk_actions')) {
|
||||
case 'labels':
|
||||
$this->authorize('view', Asset::class);
|
||||
|
||||
return (new Label)
|
||||
->with('assets', $assets)
|
||||
return view('hardware/labels')
|
||||
->with('assets', Asset::find($asset_ids))
|
||||
->with('settings', Setting::getSettings())
|
||||
->with('bulkedit', true)
|
||||
->with('count', 0);
|
||||
|
||||
case 'delete':
|
||||
$this->authorize('delete', Asset::class);
|
||||
$assets->each(function ($assets) {
|
||||
$this->authorize('delete', $assets);
|
||||
});
|
||||
|
||||
return view('hardware/bulk-delete')->with('assets', $assets);
|
||||
|
||||
case 'restore':
|
||||
$this->authorize('update', Asset::class);
|
||||
$assets = Asset::withTrashed()->find($asset_ids);
|
||||
$assets = Asset::with('assignedTo', 'location')->find($asset_ids);
|
||||
$assets->each(function ($asset) {
|
||||
$this->authorize('delete', $asset);
|
||||
});
|
||||
return view('hardware/bulk-restore')->with('assets', $assets);
|
||||
|
||||
return view('hardware/bulk-delete')->with('assets', $assets);
|
||||
case 'edit':
|
||||
$this->authorize('update', Asset::class);
|
||||
|
||||
return view('hardware/bulk')
|
||||
->with('assets', $asset_ids)
|
||||
->with('statuslabel_list', Helper::statusLabelList())
|
||||
->with('models', $models->pluck(['model']))
|
||||
->with('modelNames', $modelNames);
|
||||
->with('statuslabel_list', Helper::statusLabelList());
|
||||
}
|
||||
}
|
||||
|
||||
switch ($sort_override) {
|
||||
case 'model':
|
||||
$assets->OrderModels($order);
|
||||
break;
|
||||
case 'model_number':
|
||||
$assets->OrderModelNumber($order);
|
||||
break;
|
||||
case 'category':
|
||||
$assets->OrderCategory($order);
|
||||
break;
|
||||
case 'manufacturer':
|
||||
$assets->OrderManufacturer($order);
|
||||
break;
|
||||
case 'company':
|
||||
$assets->OrderCompany($order);
|
||||
break;
|
||||
case 'location':
|
||||
$assets->OrderLocation($order);
|
||||
case 'rtd_location':
|
||||
$assets->OrderRtdLocation($order);
|
||||
break;
|
||||
case 'status_label':
|
||||
$assets->OrderStatus($order);
|
||||
break;
|
||||
case 'supplier':
|
||||
$assets->OrderSupplier($order);
|
||||
break;
|
||||
case 'assigned_to':
|
||||
$assets->OrderAssigned($order);
|
||||
break;
|
||||
default:
|
||||
$assets->orderBy($column_sort, $order);
|
||||
break;
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'No action selected');
|
||||
}
|
||||
|
||||
@@ -196,35 +72,14 @@ class BulkAssetsController extends Controller
|
||||
public function update(Request $request)
|
||||
{
|
||||
$this->authorize('update', Asset::class);
|
||||
$has_errors = 0;
|
||||
$error_array = array();
|
||||
|
||||
// Get the back url from the session and then destroy the session
|
||||
$bulk_back_url = route('hardware.index');
|
||||
\Log::debug($request->input('ids'));
|
||||
|
||||
if ($request->session()->has('bulk_back_url')) {
|
||||
$bulk_back_url = $request->session()->pull('bulk_back_url');
|
||||
if (! $request->filled('ids') || count($request->input('ids')) <= 0) {
|
||||
return redirect()->route('hardware.index')->with('warning', trans('No assets selected, so nothing was updated.'));
|
||||
}
|
||||
|
||||
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
|
||||
|
||||
|
||||
if (! $request->filled('ids') || count($request->input('ids')) == 0) {
|
||||
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.update.no_assets_selected'));
|
||||
}
|
||||
|
||||
|
||||
$assets = Asset::whereIn('id', array_keys($request->input('ids')))->get();
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* If ANY of these are filled, prepare to update the values on the assets.
|
||||
*
|
||||
* Additional checks will be needed for some of them to make sure the values
|
||||
* make sense (for example, changing the status ID to something incompatible with
|
||||
* its checkout status.
|
||||
*/
|
||||
$assets = array_keys($request->input('ids'));
|
||||
|
||||
if (($request->filled('purchase_date'))
|
||||
|| ($request->filled('expected_checkin'))
|
||||
@@ -237,53 +92,22 @@ class BulkAssetsController extends Controller
|
||||
|| ($request->filled('company_id'))
|
||||
|| ($request->filled('status_id'))
|
||||
|| ($request->filled('model_id'))
|
||||
|| ($request->filled('next_audit_date'))
|
||||
|| ($request->filled('null_purchase_date'))
|
||||
|| ($request->filled('null_expected_checkin_date'))
|
||||
|| ($request->filled('null_next_audit_date'))
|
||||
|| ($request->anyFilled($custom_field_columns))
|
||||
|
||||
) {
|
||||
// Let's loop through those assets and build an update array
|
||||
foreach ($assets as $asset) {
|
||||
foreach ($assets as $assetId) {
|
||||
|
||||
$this->update_array = [];
|
||||
|
||||
/**
|
||||
* Leave out model_id and status here because we do math on that later. We have to do some extra
|
||||
* validation and checks on those two.
|
||||
*
|
||||
* It's tempting to make these match the request check above, but some of these values require
|
||||
* extra work to make sure the data makes sense.
|
||||
*/
|
||||
$this->conditionallyAddItem('purchase_date')
|
||||
->conditionallyAddItem('expected_checkin')
|
||||
->conditionallyAddItem('model_id')
|
||||
->conditionallyAddItem('order_number')
|
||||
->conditionallyAddItem('requestable')
|
||||
->conditionallyAddItem('status_id')
|
||||
->conditionallyAddItem('supplier_id')
|
||||
->conditionallyAddItem('warranty_months')
|
||||
->conditionallyAddItem('next_audit_date');
|
||||
foreach ($custom_field_columns as $key => $custom_field_column) {
|
||||
$this->conditionallyAddItem($custom_field_column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blank out fields that were requested to be blanked out via checkbox
|
||||
*/
|
||||
if ($request->input('null_purchase_date')=='1') {
|
||||
$this->update_array['purchase_date'] = null;
|
||||
}
|
||||
|
||||
if ($request->input('null_expected_checkin_date')=='1') {
|
||||
$this->update_array['expected_checkin'] = null;
|
||||
}
|
||||
|
||||
if ($request->input('null_next_audit_date')=='1') {
|
||||
$this->update_array['next_audit_date'] = null;
|
||||
}
|
||||
->conditionallyAddItem('warranty_months');
|
||||
|
||||
if ($request->filled('purchase_cost')) {
|
||||
$this->update_array['purchase_cost'] = $request->input('purchase_cost');
|
||||
$this->update_array['purchase_cost'] = Helper::ParseCurrency($request->input('purchase_cost'));
|
||||
}
|
||||
|
||||
if ($request->filled('company_id')) {
|
||||
@@ -293,154 +117,41 @@ class BulkAssetsController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We're trying to change the model ID - we need to do some extra checks here to make sure
|
||||
* the custom field values work for the custom fieldset rules around this asset. Uniqueness
|
||||
* and requiredness across the fieldset is particularly important, since those are
|
||||
* fieldset-specific attributes.
|
||||
*/
|
||||
if ($request->filled('model_id')) {
|
||||
$this->update_array['model_id'] = AssetModel::find($request->input('model_id'))->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* We're trying to change the status ID - we need to do some extra checks here to
|
||||
* make sure the status label type is one that makes sense for the state of the asset,
|
||||
* for example, we shouldn't be able to make an asset archived if it's currently assigned
|
||||
* to someone/something.
|
||||
*/
|
||||
if ($request->filled('status_id')) {
|
||||
$updated_status = Statuslabel::find($request->input('status_id'));
|
||||
|
||||
// We cannot assign a non-deployable status type if the asset is already assigned.
|
||||
// This could probably be added to a form request.
|
||||
// If the asset isn't assigned, we don't care what the status is.
|
||||
// Otherwise we need to make sure the status type is still a deployable one.
|
||||
if (
|
||||
($asset->assigned_to == '')
|
||||
|| ($updated_status->deployable == '1') && ($asset->assetstatus->deployable == '1')
|
||||
) {
|
||||
$this->update_array['status_id'] = $updated_status->id;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* We're changing the location ID - figure out which location we should apply
|
||||
* this change to:
|
||||
*
|
||||
* 0 - RTD location only
|
||||
* 1 - location ID and RTD location ID
|
||||
* 2 - location ID only
|
||||
*
|
||||
* Note: this is kinda dumb and we should just use human-readable values IMHO. - snipe
|
||||
*/
|
||||
if ($request->filled('rtd_location_id')) {
|
||||
|
||||
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '0')) {
|
||||
$this->update_array['rtd_location_id'] = $request->input('rtd_location_id');
|
||||
}
|
||||
|
||||
$this->update_array['rtd_location_id'] = $request->input('rtd_location_id');
|
||||
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '1')) {
|
||||
$this->update_array['location_id'] = $request->input('rtd_location_id');
|
||||
$this->update_array['rtd_location_id'] = $request->input('rtd_location_id');
|
||||
}
|
||||
|
||||
if (($request->filled('update_real_loc')) && (($request->input('update_real_loc')) == '2')) {
|
||||
$this->update_array['location_id'] = $request->input('rtd_location_id');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ------------------------------------------------------------------------------
|
||||
* ANYTHING that happens past this foreach
|
||||
* WILL NOT BE logged in the edit log_meta data
|
||||
* ------------------------------------------------------------------------------
|
||||
*/
|
||||
$changed = [];
|
||||
$asset = Asset::where('id' ,$assetId)->get();
|
||||
|
||||
foreach ($this->update_array as $key => $value) {
|
||||
|
||||
if ($this->update_array[$key] != $asset->{$key}) {
|
||||
$changed[$key]['old'] = $asset->{$key};
|
||||
if ($this->update_array[$key] != $asset->toArray()[0][$key]) {
|
||||
$changed[$key]['old'] = $asset->toArray()[0][$key];
|
||||
$changed[$key]['new'] = $this->update_array[$key];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Start all the custom fields shenanigans
|
||||
*/
|
||||
$logAction = new Actionlog();
|
||||
$logAction->item_type = Asset::class;
|
||||
$logAction->item_id = $assetId;
|
||||
$logAction->created_at = date("Y-m-d H:i:s");
|
||||
$logAction->user_id = Auth::id();
|
||||
$logAction->log_meta = json_encode($changed);
|
||||
$logAction->logaction('update');
|
||||
|
||||
// Does the model have a fieldset?
|
||||
if ($asset->model->fieldset) {
|
||||
foreach ($asset->model->fieldset->fields as $field) {
|
||||
DB::table('assets')
|
||||
->where('id', $assetId)
|
||||
->update($this->update_array);
|
||||
} // endforeach
|
||||
|
||||
if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) {
|
||||
$decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
|
||||
|
||||
/*
|
||||
* Check if the decrypted existing value is different from one we just submitted
|
||||
* and if not, pull it out of the object since it shouldn't really be updating at all.
|
||||
* If we don't do this, it will try to re-encrypt it, and the same value encrypted two
|
||||
* different times will have different values, so it will *look* like it was updated
|
||||
* but it wasn't.
|
||||
*/
|
||||
if ($decrypted_old != $this->update_array[$field->db_column]) {
|
||||
$asset->{$field->db_column} = \Crypt::encrypt($this->update_array[$field->db_column]);
|
||||
} else {
|
||||
/*
|
||||
* Remove the encrypted custom field from the update_array, since nothing changed
|
||||
*/
|
||||
unset($this->update_array[$field->db_column]);
|
||||
unset($asset->{$field->db_column});
|
||||
}
|
||||
|
||||
/*
|
||||
* These custom fields aren't encrypted, just carry on as usual
|
||||
*/
|
||||
} else {
|
||||
|
||||
if ((array_key_exists($field->db_column, $this->update_array)) && ($asset->{$field->db_column} != $this->update_array[$field->db_column])) {
|
||||
|
||||
// Check if this is an array, and if so, flatten it
|
||||
if (is_array($this->update_array[$field->db_column])) {
|
||||
$asset->{$field->db_column} = implode(', ', $this->update_array[$field->db_column]);
|
||||
} else {
|
||||
$asset->{$field->db_column} = $this->update_array[$field->db_column];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // endforeach
|
||||
}
|
||||
|
||||
|
||||
// Check if it passes validation, and then try to save
|
||||
if (!$asset->update($this->update_array)) {
|
||||
|
||||
// Build the error array
|
||||
foreach ($asset->getErrors()->toArray() as $key => $message) {
|
||||
for ($x = 0; $x < count($message); $x++) {
|
||||
$error_array[$key][] = trans('general.asset') . ' ' . $asset->id . ': ' . $message[$x];
|
||||
$has_errors++;
|
||||
}
|
||||
}
|
||||
|
||||
} // end if saved
|
||||
|
||||
} // end asset foreach
|
||||
|
||||
if ($has_errors > 0) {
|
||||
return redirect($bulk_back_url)->with('bulk_asset_errors', $error_array);
|
||||
}
|
||||
|
||||
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success'));
|
||||
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.update.success'));
|
||||
// no values given, nothing to update
|
||||
}
|
||||
// no values given, nothing to update
|
||||
return redirect($bulk_back_url)->with('warning', trans('admin/hardware/message.update.nothing_updated'));
|
||||
|
||||
return redirect()->route('hardware.index')->with('warning', trans('admin/hardware/message.update.nothing_updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -477,11 +188,6 @@ class BulkAssetsController extends Controller
|
||||
{
|
||||
$this->authorize('delete', Asset::class);
|
||||
|
||||
$bulk_back_url = route('hardware.index');
|
||||
if ($request->session()->has('bulk_back_url')) {
|
||||
$bulk_back_url = $request->session()->pull('bulk_back_url');
|
||||
}
|
||||
|
||||
if ($request->filled('ids')) {
|
||||
$assets = Asset::find($request->get('ids'));
|
||||
foreach ($assets as $asset) {
|
||||
@@ -493,11 +199,11 @@ class BulkAssetsController extends Controller
|
||||
->update($update_array);
|
||||
} // endforeach
|
||||
|
||||
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.delete.success'));
|
||||
return redirect()->to('hardware')->with('success', trans('admin/hardware/message.delete.success'));
|
||||
// no values given, nothing to update
|
||||
}
|
||||
|
||||
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.delete.nothing_updated'));
|
||||
return redirect()->to('hardware')->with('info', trans('admin/hardware/message.delete.nothing_updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -516,18 +222,15 @@ class BulkAssetsController extends Controller
|
||||
* Process Multiple Checkout Request
|
||||
* @return View
|
||||
*/
|
||||
public function storeCheckout(AssetCheckoutRequest $request)
|
||||
public function storeCheckout(Request $request)
|
||||
{
|
||||
|
||||
$this->authorize('checkout', Asset::class);
|
||||
|
||||
try {
|
||||
$admin = Auth::user();
|
||||
|
||||
$target = $this->determineCheckoutTarget();
|
||||
|
||||
if (! is_array($request->get('selected_assets'))) {
|
||||
return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans('admin/hardware/message.checkout.no_assets_selected'));
|
||||
return redirect()->route('hardware/bulkcheckout')->withInput()->with('error', trans('admin/hardware/message.checkout.no_assets_selected'));
|
||||
}
|
||||
|
||||
$asset_ids = array_filter($request->get('selected_assets'));
|
||||
@@ -555,8 +258,7 @@ class BulkAssetsController extends Controller
|
||||
foreach ($asset_ids as $asset_id) {
|
||||
$asset = Asset::findOrFail($asset_id);
|
||||
$this->authorize('checkout', $asset);
|
||||
|
||||
$error = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), $asset->name, null);
|
||||
$error = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), null);
|
||||
|
||||
if ($target->location_id != '') {
|
||||
$asset->location_id = $target->location_id;
|
||||
@@ -575,23 +277,9 @@ class BulkAssetsController extends Controller
|
||||
return redirect()->to('hardware')->with('success', trans('admin/hardware/message.checkout.success'));
|
||||
}
|
||||
// Redirect to the asset management page with error
|
||||
return redirect()->route('hardware.bulkcheckout.show')->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($errors);
|
||||
return redirect()->to('hardware/bulk-checkout')->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($errors);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return redirect()->route('hardware.bulkcheckout.show')->with('error', $e->getErrors());
|
||||
}
|
||||
|
||||
}
|
||||
public function restore(Request $request) {
|
||||
$this->authorize('update', Asset::class);
|
||||
$assetIds = $request->get('ids');
|
||||
if (empty($assetIds)) {
|
||||
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.restore.nothing_updated'));
|
||||
} else {
|
||||
foreach ($assetIds as $key => $assetId) {
|
||||
$asset = Asset::withTrashed()->find($assetId);
|
||||
$asset->restore();
|
||||
}
|
||||
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success'));
|
||||
return redirect()->to('hardware/bulk-checkout')->with('error', $e->getErrors());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SamlNonce;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Models\Ldap;
|
||||
@@ -57,6 +56,7 @@ class LoginController extends Controller
|
||||
parent::__construct();
|
||||
$this->middleware('guest', ['except' => ['logout', 'postTwoFactorAuth', 'getTwoFactorAuth', 'getTwoFactorEnroll']]);
|
||||
Session::put('backUrl', \URL::previous());
|
||||
// $this->ldap = $ldap;
|
||||
$this->saml = $saml;
|
||||
}
|
||||
|
||||
@@ -68,17 +68,15 @@ class LoginController extends Controller
|
||||
return redirect()->intended('/');
|
||||
}
|
||||
|
||||
if (!$request->session()->has('loggedout')) {
|
||||
// If the environment is set to ALWAYS require SAML, go straight to the SAML route.
|
||||
// We don't need to check other settings, as this should override those.
|
||||
if (config('app.require_saml')) {
|
||||
return redirect()->route('saml.login');
|
||||
}
|
||||
// If the environment is set to ALWAYS require SAML, go straight to the SAML route.
|
||||
// We don't need to check other settings, as this should override those.
|
||||
if (config('app.require_saml')) {
|
||||
return redirect()->route('saml.login');
|
||||
}
|
||||
|
||||
|
||||
if ($this->saml->isEnabled() && Setting::getSettings()->saml_forcelogin == '1' && ! ($request->has('nosaml') || $request->session()->has('error'))) {
|
||||
return redirect()->route('saml.login');
|
||||
}
|
||||
if ($this->saml->isEnabled() && Setting::getSettings()->saml_forcelogin == '1' && ! ($request->has('nosaml') || $request->session()->has('error'))) {
|
||||
return redirect()->route('saml.login');
|
||||
}
|
||||
|
||||
if (Setting::getSettings()->login_common_disabled == '1') {
|
||||
@@ -105,53 +103,28 @@ class LoginController extends Controller
|
||||
{
|
||||
$saml = $this->saml;
|
||||
$samlData = $request->session()->get('saml_login');
|
||||
|
||||
if ($saml->isEnabled() && ! empty($samlData)) {
|
||||
|
||||
try {
|
||||
Log::debug('Attempting to log user in by SAML authentication.');
|
||||
$user = $saml->samlLogin($samlData);
|
||||
$notValidAfter = new \Carbon\Carbon(@$samlData['assertionNotOnOrAfter']);
|
||||
if(\Carbon::now()->greaterThanOrEqualTo($notValidAfter)) {
|
||||
abort(400,"Expired SAML Assertion");
|
||||
}
|
||||
if(SamlNonce::where('nonce', @$samlData['nonce'])->count() > 0) {
|
||||
abort(400,"Assertion has already been used");
|
||||
}
|
||||
Log::debug("okay, fine, this is a new nonce then. Good for you.");
|
||||
if (!is_null($user)) {
|
||||
if (! is_null($user)) {
|
||||
Auth::login($user);
|
||||
} else {
|
||||
$username = $saml->getUsername();
|
||||
\Log::debug("SAML user '$username' could not be found in database.");
|
||||
\Log::warning("SAML user '$username' could not be found in database.");
|
||||
$request->session()->flash('error', trans('auth/message.signin.error'));
|
||||
$saml->clearData();
|
||||
}
|
||||
|
||||
if ($user = Auth::user()) {
|
||||
$user->last_login = \Carbon::now();
|
||||
$user->saveQuietly();
|
||||
$user->save();
|
||||
}
|
||||
$s = new SamlNonce();
|
||||
$s->nonce = @$samlData['nonce'];
|
||||
$s->not_valid_after = $notValidAfter;
|
||||
$s->save();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
\Log::debug('There was an error authenticating the SAML user: '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Fallthrough with better logging
|
||||
} else {
|
||||
|
||||
// Better logging
|
||||
if (empty($samlData)) {
|
||||
\Log::debug("SAML page requested, but samlData seems empty.");
|
||||
\Log::warning('There was an error authenticating the SAML user: '.$e->getMessage());
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +162,7 @@ class LoginController extends Controller
|
||||
Log::debug("Local user ".$request->input('username')." does not exist");
|
||||
Log::debug("Creating local user ".$request->input('username'));
|
||||
|
||||
if ($user = Ldap::createUserFromLdap($ldap_user, $request->input('password'))) {
|
||||
if ($user = Ldap::createUserFromLdap($ldap_user)) { //this handles passwords on its own
|
||||
Log::debug("Local user created.");
|
||||
} else {
|
||||
Log::debug("Could not create local user.");
|
||||
@@ -201,15 +174,13 @@ class LoginController extends Controller
|
||||
|
||||
$ldap_attr = Ldap::parseAndMapLdapAttributes($ldap_user);
|
||||
|
||||
$user->password = $user->noPassword();
|
||||
if (Setting::getSettings()->ldap_pw_sync=='1') {
|
||||
$user->password = bcrypt($request->input('password'));
|
||||
}
|
||||
|
||||
$user->email = $ldap_attr['email'];
|
||||
$user->first_name = $ldap_attr['firstname'];
|
||||
$user->last_name = $ldap_attr['lastname']; //FIXME (or TODO?) - do we need to map additional fields that we now support? E.g. country, phone, etc.
|
||||
$user->saveQuietly();
|
||||
$user->save();
|
||||
} // End if(!user)
|
||||
return $user;
|
||||
}
|
||||
@@ -268,12 +239,10 @@ class LoginController extends Controller
|
||||
|
||||
//If the environment is set to ALWAYS require SAML, return access denied
|
||||
if (config('app.require_saml')) {
|
||||
\Log::debug('require SAML is enabled in the .env - return a 403');
|
||||
return view('errors.403');
|
||||
}
|
||||
|
||||
if (Setting::getSettings()->login_common_disabled == '1') {
|
||||
\Log::debug('login_common_disabled is set to 1 - return a 403');
|
||||
return view('errors.403');
|
||||
}
|
||||
|
||||
@@ -329,7 +298,7 @@ class LoginController extends Controller
|
||||
if ($user = Auth::user()) {
|
||||
$user->last_login = \Carbon::now();
|
||||
$user->activated = 1;
|
||||
$user->saveQuietly();
|
||||
$user->save();
|
||||
}
|
||||
// Redirect to the users page
|
||||
return redirect()->intended()->with('success', trans('auth/message.signin.success'));
|
||||
@@ -364,6 +333,7 @@ class LoginController extends Controller
|
||||
|
||||
$secret = Google2FA::generateSecretKey();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->save();
|
||||
|
||||
$barcode = new Barcode();
|
||||
$barcode_obj =
|
||||
@@ -381,8 +351,6 @@ class LoginController extends Controller
|
||||
[-2, -2, -2, -2]
|
||||
);
|
||||
|
||||
$user->saveQuietly(); // make sure to save *AFTER* displaying the barcode, or else we might save a two_factor_secret that we never actually displayed to the user if the barcode fails
|
||||
|
||||
return view('auth.two_factor_enroll')->with('barcode_obj', $barcode_obj);
|
||||
}
|
||||
|
||||
@@ -427,7 +395,7 @@ class LoginController extends Controller
|
||||
return redirect()->route('two-factor')->with('error', trans('auth/message.two_factor.code_required'));
|
||||
}
|
||||
|
||||
if (! $request->has('two_factor_secret')) { // TODO this seems almost the same as above?
|
||||
if (! $request->has('two_factor_secret')) {
|
||||
return redirect()->route('two-factor')->with('error', 'Two-factor code is required.');
|
||||
}
|
||||
|
||||
@@ -436,7 +404,7 @@ class LoginController extends Controller
|
||||
|
||||
if (Google2FA::verifyKey($user->two_factor_secret, $secret)) {
|
||||
$user->two_factor_enrolled = 1;
|
||||
$user->saveQuietly();
|
||||
$user->save();
|
||||
$request->session()->put('2fa_authed', $user->id);
|
||||
|
||||
return redirect()->route('home')->with('success', 'You are logged in!');
|
||||
@@ -455,17 +423,10 @@ class LoginController extends Controller
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
// Logout is only allowed with a http POST but we need to allow GET for SAML SLO
|
||||
$settings = Setting::getSettings();
|
||||
$saml = $this->saml;
|
||||
$samlLogout = $request->session()->get('saml_logout');
|
||||
$sloRedirectUrl = null;
|
||||
$sloRequestUrl = null;
|
||||
|
||||
// Only allow GET if we are doing SAML SLO otherwise abort with 405
|
||||
if ($request->isMethod('GET') && !$samlLogout) {
|
||||
abort(405);
|
||||
}
|
||||
|
||||
if ($saml->isEnabled()) {
|
||||
$auth = $saml->getAuth();
|
||||
@@ -484,10 +445,7 @@ class LoginController extends Controller
|
||||
|
||||
$request->session()->regenerate(true);
|
||||
|
||||
if ($request->session()->has('password_hash_'.Auth::getDefaultDriver())){
|
||||
$request->session()->remove('password_hash_'.Auth::getDefaultDriver());
|
||||
}
|
||||
|
||||
$request->session()->regenerate(true);
|
||||
Auth::logout();
|
||||
|
||||
if (! empty($sloRedirectUrl)) {
|
||||
@@ -517,7 +475,6 @@ class LoginController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function username()
|
||||
{
|
||||
return 'username';
|
||||
@@ -544,7 +501,6 @@ class LoginController extends Controller
|
||||
->withErrors([$this->username() => $message]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Override the lockout time and duration
|
||||
*
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\SaveUserRequest;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
@@ -41,7 +43,6 @@ class ResetPasswordController extends Controller
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
$this->middleware('throttle:10,1');
|
||||
}
|
||||
|
||||
protected function rules()
|
||||
@@ -62,14 +63,6 @@ class ResetPasswordController extends Controller
|
||||
|
||||
public function showResetForm(Request $request, $token = null)
|
||||
{
|
||||
|
||||
$credentials = $request->only('email', 'token');
|
||||
|
||||
if (is_null($this->broker()->getUser($credentials))) {
|
||||
\Log::debug('Password reset form FAILED - this token is not valid.');
|
||||
return redirect()->route('password.request')->with('error', trans('passwords.token'));
|
||||
}
|
||||
|
||||
return view('auth.passwords.reset')->with(
|
||||
[
|
||||
'token' => $token,
|
||||
@@ -80,53 +73,38 @@ class ResetPasswordController extends Controller
|
||||
|
||||
public function reset(Request $request)
|
||||
{
|
||||
|
||||
$broker = $this->broker();
|
||||
|
||||
$messages = [
|
||||
'password.not_in' => trans('validation.disallow_same_pwd_as_user_fields'),
|
||||
];
|
||||
|
||||
$request->validate($this->rules(), $request->all(), $this->validationErrorMessages());
|
||||
|
||||
\Log::debug('Checking if '.$request->input('username').' exists');
|
||||
// Check to see if the user even exists - we'll treat the response the same to prevent user sniffing
|
||||
if ($user = User::where('username', '=', $request->input('username'))->where('activated', '1')->whereNotNull('email')->first()) {
|
||||
\Log::debug($user->username.' exists');
|
||||
|
||||
|
||||
// handle the password validation rules set by the admin settings
|
||||
if (strpos(Setting::passwordComplexityRulesSaving('store'), 'disallow_same_pwd_as_user_fields') !== false) {
|
||||
$request->validate(
|
||||
[
|
||||
'password' => 'required|notIn:["'.$user->email.'","'.$user->username.'","'.$user->first_name.'","'.$user->last_name.'"',
|
||||
], $messages);
|
||||
}
|
||||
|
||||
|
||||
// set the response
|
||||
$response = $broker->reset(
|
||||
$this->credentials($request), function ($user, $password) {
|
||||
$this->resetPassword($user, $password);
|
||||
});
|
||||
|
||||
// Check if the password reset above actually worked
|
||||
if ($response == \Password::PASSWORD_RESET) {
|
||||
\Log::debug('Password reset for '.$user->username.' worked');
|
||||
return redirect()->guest('login')->with('success', trans('passwords.reset'));
|
||||
}
|
||||
|
||||
\Log::debug('Password reset for '.$user->username.' FAILED - this user exists but the token is not valid');
|
||||
return redirect()->back()->withInput($request->only('email'))->with('success', trans('passwords.reset'));
|
||||
// Check to see if the user even exists
|
||||
$user = User::where('username', '=', $request->input('username'))->first();
|
||||
|
||||
$broker = $this->broker();
|
||||
if (strpos(Setting::passwordComplexityRulesSaving('store'), 'disallow_same_pwd_as_user_fields') !== false) {
|
||||
$request->validate(
|
||||
[
|
||||
'password' => 'required|notIn:["'.$user->email.'","'.$user->username.'","'.$user->first_name.'","'.$user->last_name.'"',
|
||||
], $messages);
|
||||
}
|
||||
|
||||
$response = $broker->reset(
|
||||
$this->credentials($request), function ($user, $password) {
|
||||
$this->resetPassword($user, $password);
|
||||
}
|
||||
);
|
||||
|
||||
\Log::debug('Password reset for '.$request->input('username').' FAILED - user does not exist or does not have an email address - but make it look like it succeeded');
|
||||
return redirect()->guest('login')->with('success', trans('passwords.reset'));
|
||||
|
||||
return $response == \Password::PASSWORD_RESET
|
||||
? $this->sendResetResponse($request, $response)
|
||||
: $this->sendResetFailedResponse($request, $response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected function sendResetFailedResponse(Request $request, $response)
|
||||
{
|
||||
return redirect()->back()
|
||||
->withInput(['username'=> $request->input('username')])
|
||||
->withErrors(['username' => trans($response), 'password' => trans($response)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ class SamlController extends Controller
|
||||
$metadata = $this->saml->getSPMetadata();
|
||||
|
||||
if (empty($metadata)) {
|
||||
\Log::debug('SAML metadata is empty - return a 403');
|
||||
return response()->view('errors.403', [], 403);
|
||||
}
|
||||
|
||||
@@ -142,6 +141,6 @@ class SamlController extends Controller
|
||||
return view('errors.403');
|
||||
}
|
||||
|
||||
return redirect()->route('logout.get')->with(['saml_logout' => true,'saml_slo_redirect_url' => $sloUrl]);
|
||||
return redirect()->route('logout')->with('saml_slo_redirect_url', $sloUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class BulkAssetModelsController extends Controller
|
||||
AssetModel::whereIn('id', $models_raw_array)->update($update_array);
|
||||
|
||||
return redirect()->route('models.index')
|
||||
->with('success', trans_choice('admin/models/message.bulkedit.success', count($models_raw_array), ['model_count' => count($models_raw_array)]));
|
||||
->with('success', trans('admin/models/message.bulkedit.success'));
|
||||
}
|
||||
|
||||
return redirect()->route('models.index')
|
||||
|
||||
@@ -60,9 +60,6 @@ final class CompaniesController extends Controller
|
||||
|
||||
$company = new Company;
|
||||
$company->name = $request->input('name');
|
||||
$company->phone = $request->input('phone');
|
||||
$company->fax = $request->input('fax');
|
||||
$company->email = $request->input('email');
|
||||
|
||||
$company = $request->handleImages($company);
|
||||
|
||||
@@ -114,9 +111,6 @@ final class CompaniesController extends Controller
|
||||
$this->authorize('update', $company);
|
||||
|
||||
$company->name = $request->input('name');
|
||||
$company->phone = $request->input('phone');
|
||||
$company->fax = $request->input('fax');
|
||||
$company->email = $request->input('email');
|
||||
|
||||
$company = $request->handleImages($company);
|
||||
|
||||
@@ -125,7 +119,8 @@ final class CompaniesController extends Controller
|
||||
->with('success', trans('admin/companies/message.update.success'));
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->withErrors($company->getErrors());
|
||||
return redirect()->route('companies.edit', ['company' => $companyId])
|
||||
->with('error', trans('admin/companies/message.update.error'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user