Compare commits

..

4 Commits

Author SHA1 Message Date
snipe
f98f978502 Fixed typos
Signed-off-by: snipe <snipe@snipe.net>
2024-12-11 16:53:15 +00:00
snipe
a0bbafeb30 Removed unused code
Signed-off-by: snipe <snipe@snipe.net>
2024-12-10 14:35:22 +00:00
snipe
dbd33e1a34 Check for specific error message
Signed-off-by: snipe <snipe@snipe.net>
2024-12-10 14:34:16 +00:00
snipe
8afba32169 Validate group data
Signed-off-by: snipe <snipe@snipe.net>
2024-12-10 14:23:23 +00:00
4163 changed files with 170922 additions and 241810 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -11,12 +11,12 @@ MYSQL_ROOT_PASSWORD=changeme1234
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=develop
APP_DEBUG=true
APP_DEBUG=false
# please regenerate the APP_KEY value by calling `docker-compose run --rm snipeit bash` and then `php artisan key:generate --show` and then copy paste the value here
APP_KEY=base64:3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ=
APP_URL=http://localhost:8000
APP_TIMEZONE='UTC'
APP_LOCALE=en-US
APP_LOCALE=en
MAX_RESULTS=500
# --------------------------------------------
@@ -35,7 +35,6 @@ DB_USERNAME=snipeit
DB_PASSWORD=changeme1234
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_DUMP_SKIP_SSL=true
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
@@ -79,13 +78,6 @@ MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
# --------------------------------------------
# OPTIONAL: CHANGE PHP UPLOAD LIMITS (UNCOMMENT WHEN NEEDING TO BE CHANGED)
# --------------------------------------------
#PHP_UPLOAD_LIMIT=10
#PHP_POST_MAX_SIZE=10
#PHP_UPLOAD_MAX_FILESIZE=10
#PHP_MEMORY_LIMIT=10
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
@@ -166,7 +158,7 @@ RESET_PASSWORD_LINK_EXPIRES=900
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=single
LOG_CHANNEL=stderr
LOG_MAX_DAYS=10
APP_LOCKED=false
APP_CIPHER=AES-256-CBC

View File

@@ -1,7 +1,7 @@
# --------------------------------------------
# REQUIRED: DOCKER SPECIFIC SETTINGS
# --------------------------------------------
APP_VERSION=
APP_VERSION=v6.4.1
APP_PORT=8000
# --------------------------------------------
@@ -9,7 +9,7 @@ APP_PORT=8000
# --------------------------------------------
APP_ENV=production
APP_DEBUG=false
# Please regenerate the APP_KEY value by calling `docker compose run --rm app php artisan key:generate --show`. Copy paste the value here
# Please regenerate the APP_KEY value by calling `docker compose run --rm snipeit php artisan key:generate --show`. Copy paste the value here
APP_KEY=base64:3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ=
APP_URL=http://localhost:8000
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - TZ identifier
@@ -28,7 +28,6 @@ PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=db
DB_SOCKET=null
DB_PORT='3306'
DB_DATABASE=snipeit
DB_USERNAME=snipeit
@@ -36,7 +35,6 @@ DB_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_DUMP_SKIP_SSL=true
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
@@ -85,15 +83,6 @@ MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
# --------------------------------------------
# OPTIONAL: CHANGE PHP UPLOAD LIMITS (UNCOMMENT WHEN NEEDING TO BE CHANGED)
# --------------------------------------------
#PHP_UPLOAD_LIMIT=10
#PHP_POST_MAX_SIZE=10
#PHP_UPLOAD_MAX_FILESIZE=10
#PHP_MEMORY_LIMIT=10
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
@@ -169,7 +158,6 @@ AWS_DEFAULT_REGION=null
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60
RESET_PASSWORD_LINK_EXPIRES=900
INVITE_PASSWORD_LINK_EXPIRES=1500
# --------------------------------------------
# OPTIONAL: MISC

View File

@@ -24,14 +24,12 @@ PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_SOCKET=null
DB_PORT=3306
DB_DATABASE=null
DB_USERNAME=null
DB_PASSWORD=null
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_DUMP_SKIP_SSL=false
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
DB_SANITIZE_BY_DEFAULT=false
@@ -82,12 +80,6 @@ MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
ALLOW_BACKUP_DELETE=false
ALLOW_DATA_PURGE=false
ALL_BACKUP_KEEP_DAYS=7
DAILY_BACKUP_KEEP_DAYS=16
WEEKLY_BACKUP_KEEP_WEEKS=8
MONTHLY_BACKUP_KEEP_MONTHS=4
YEARLY_BACKUP_KEEP_YEARS=2
BACKUP_PURGE_OLDEST_AT_MEGS=5000
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
@@ -101,7 +93,7 @@ PASSPORT_COOKIE_NAME='snipeit_passport_token'
COOKIE_DOMAIN=null
SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=15
BS_TABLE_STORAGE=localStorage
BS_TABLE_STORAGE=cookieStorage
BS_TABLE_DEEPLINK=true
# --------------------------------------------
@@ -175,13 +167,11 @@ LOGIN_AUTOCOMPLETE=false
RESET_PASSWORD_LINK_EXPIRES=15
PASSWORD_CONFIRM_TIMEOUT=10800
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
INVITE_PASSWORD_LINK_EXPIRES=1500
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=single
LOG_DEPRECATIONS=false
LOG_MAX_DAYS=10
APP_LOCKED=false
APP_CIPHER=AES-256-CBC

View File

@@ -10,10 +10,10 @@ name: Codacy Security Scan
on:
push:
branches: [ develop ]
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ develop ]
branches: [ master ]
schedule:
- cron: '36 23 * * 3'
@@ -22,11 +22,11 @@ permissions:
jobs:
codacy-security-scan:
# Ensure schedule job never runs on forked repos. It's only executed for 'grokability/snipe-it'
# Ensure schedule job never runs on forked repos. It's only executed for 'snipe/snipe-it'
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
if: (github.repository == 'grokability/snipe-it') || ((github.repository != 'grokability/snipe-it') && (github.event_name != 'schedule'))
if: (github.repository == 'snipe/snipe-it') || ((github.repository != 'snipe/snipe-it') && (github.event_name != 'schedule'))
name: Codacy Security Scan
runs-on: ubuntu-latest
steps:
@@ -36,7 +36,7 @@ jobs:
# 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.7
uses: codacy/codacy-analysis-cli-action@v4.4.5
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

View File

@@ -20,8 +20,8 @@ permissions:
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
if: github.repository == 'grokability/snipe-it'
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
if: github.repository == 'snipe/snipe-it'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
@@ -32,7 +32,7 @@ jobs:
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
type=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
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.

View File

@@ -1,5 +1,5 @@
# Snipe-IT Docker image build for hub.docker.com
name: Docker images (Ubuntu)
name: Docker images
# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
# Also run for PRs to ensure PR doesn't break Docker build process
@@ -20,8 +20,8 @@ permissions:
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'grokability/snipe-it'
if: github.repository == 'grokability/snipe-it'
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
if: github.repository == 'snipe/snipe-it'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
@@ -32,7 +32,7 @@ jobs:
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
type=semver,pattern=v{{major}}-latest
type=semver,pattern=v{{major}}-latest
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.

View File

@@ -25,9 +25,9 @@ jobs:
fail-fast: false
matrix:
php-version:
- "8.1"
- "8.2"
- "8.3"
- "8.4"
name: PHP ${{ matrix.php-version }}
@@ -67,7 +67,7 @@ jobs:
run: |
php artisan key:generate
php artisan migrate --force
php artisan passport:install --no-interaction
php artisan passport:install
chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit

View File

@@ -21,10 +21,9 @@ jobs:
fail-fast: false
matrix:
php-version:
- "8.1"
- "8.2"
- "8.3"
- "8.4"
name: PHP ${{ matrix.php-version }}
@@ -65,7 +64,7 @@ jobs:
run: |
php artisan key:generate
php artisan migrate --force
php artisan passport:install --no-interaction
php artisan passport:install
chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit

View File

@@ -15,7 +15,7 @@ jobs:
fail-fast: false
matrix:
php-version:
- "8.3"
- "8.1.1"
name: PHP ${{ matrix.php-version }}

View File

@@ -1,240 +0,0 @@
{
"standard": "WCAG2AA",
"level": "error",
"defaults": {
"useIncognitoBrowserContext": false,
"timeout": 500000,
"wait": 5000,
"ignore" : [
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G145.Fail",
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
],
"viewport": {
"width": 1280,
"height": 1024
}
},
"urls": [
{
"__NOTE" : "this should always be FIRST (if browser context is preserved)",
"url": "https://snipe-it.test/login",
"actions": [
"navigate to https://snipe-it.test/login",
"screen capture tests/pa11y/login.png",
"set field input[name='username'] to admin",
"set field input[name='password'] to password",
"click element button[type=submit]",
"wait for url to be https://snipe-it.test/",
"screen capture tests/pa11y/dashboard.png"
]
},
{
"url" : "https://snipe-it.test/admin",
"actions" : [
"navigate to https://snipe-it.test/admin",
"screen capture tests/pa11y/admin-settings.png"
]
},
{
"url" : "https://snipe-it.test/admin/branding",
"actions" : [
"navigate to https://snipe-it.test/admin/branding",
"screen capture tests/pa11y/admin-branding.png"
]
},
{
"url" : "https://snipe-it.test/admin/general",
"actions" : [
"navigate to https://snipe-it.test/admin/general",
"screen capture tests/pa11y/admin-general.png"
]
},
{
"url" : "https://snipe-it.test/hardware/create",
"actions" : [
"navigate to https://snipe-it.test/hardware/create",
"screen capture tests/pa11y/asset-create.png"
]
},
{
"url" : "https://snipe-it.test/hardware",
"actions" : [
"navigate to https://snipe-it.test/hardware",
"screen capture tests/pa11y/asset-list.png"
]
},
{
"url" : "https://snipe-it.test/hardware/1",
"actions" : [
"navigate to https://snipe-it.test/hardware/1",
"screen capture tests/pa11y/asset-detail.png"
]
},
{
"url" : "https://snipe-it.test/account/view-assets",
"actions" : [
"navigate to https://snipe-it.test/account/view-assets",
"screen capture tests/pa11y/profile.png"
]
},
{
"url" : "https://snipe-it.test/licences",
"actions" : [
"navigate to https://snipe-it.test/licenses",
"screen capture tests/pa11y/license-list.png"
]
},
{
"url" : "https://snipe-it.test/licences/create",
"actions" : [
"navigate to https://snipe-it.test/licenses/create",
"screen capture tests/pa11y/license-create.png"
]
},
{
"url" : "https://snipe-it.test/licences/1",
"actions" : [
"navigate to https://snipe-it.test/licenses/1",
"screen capture tests/pa11y/license-view.png"
]
},
{
"url" : "https://snipe-it.test/consumables",
"actions" : [
"navigate to https://snipe-it.test/consumables",
"screen capture tests/pa11y/consumable-list.png"
]
},
{
"url" : "https://snipe-it.test/consumables/create",
"actions" : [
"navigate to https://snipe-it.test/consumables/create",
"screen capture tests/pa11y/consumable-create.png"
]
},
{
"url" : "https://snipe-it.test/consumables/1",
"actions" : [
"navigate to https://snipe-it.test/consumables/1",
"screen capture tests/pa11y/consumable-view.png"
]
},
{
"url" : "https://snipe-it.test/accessories",
"actions" : [
"navigate to https://snipe-it.test/accessories",
"screen capture tests/pa11y/accessory-list.png"
]
},
{
"url" : "https://snipe-it.test/accessories/create",
"actions" : [
"navigate to https://snipe-it.test/accessories/create",
"screen capture tests/pa11y/accessory-create.png"
]
},
{
"url" : "https://snipe-it.test/accessories/1",
"actions" : [
"navigate to https://snipe-it.test/accessories/1",
"screen capture tests/pa11y/accessory-view.png"
]
},
{
"url" : "https://snipe-it.test/locations",
"actions" : [
"navigate to https://snipe-it.test/locations",
"screen capture tests/pa11y/location-list.png"
]
},
{
"url" : "https://snipe-it.test/locations/create",
"actions" : [
"navigate to https://snipe-it.test/locations/create",
"screen capture tests/pa11y/location-create.png"
]
},
{
"url" : "https://snipe-it.test/locations/1",
"actions" : [
"navigate to https://snipe-it.test/locations/1",
"screen capture tests/pa11y/location-view.png"
]
},
{
"url" : "https://snipe-it.test/models",
"actions" : [
"navigate to https://snipe-it.test/models",
"screen capture tests/pa11y/model-list.png"
]
},
{
"url" : "https://snipe-it.test/models/create",
"actions" : [
"navigate to https://snipe-it.test/models/create",
"screen capture tests/pa11y/model-create.png"
]
},
{
"url" : "https://snipe-it.test/models/1",
"actions" : [
"navigate to https://snipe-it.test/models/1",
"screen capture tests/pa11y/model-view.png"
]
},
{
"url" : "https://snipe-it.test/companies",
"actions" : [
"navigate to https://snipe-it.test/companies",
"screen capture tests/pa11y/company-list.png"
]
},
{
"url" : "https://snipe-it.test/companies/create",
"actions" : [
"navigate to https://snipe-it.test/companies/create",
"screen capture tests/pa11y/company-create.png"
]
},
{
"url" : "https://snipe-it.test/companies/1",
"actions" : [
"navigate to https://snipe-it.test/companies/1",
"screen capture tests/pa11y/company-view.png"
]
},
{
"url" : "https://snipe-it.test/departments",
"actions" : [
"navigate to https://snipe-it.test/departments",
"screen capture tests/pa11y/department-list.png"
]
},
{
"url" : "https://snipe-it.test/departments/create",
"actions" : [
"navigate to https://snipe-it.test/departments/create",
"screen capture tests/pa11y/department-create.png"
]
},
{
"url" : "https://snipe-it.test/departments/1",
"actions" : [
"navigate to https://snipe-it.test/departments/1",
"screen capture tests/pa11y/department-view.png"
]
},
{
"url" : "https://snipe-it.test/invalid-url",
"actions" : [
"navigate to https://snipe-it.test/invalid-url",
"screen capture tests/pa11y/404.png"
]
}
]
}

View File

@@ -3,8 +3,8 @@
"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": "8.2.0",
"php_max_major_minor": "8.4",
"php_max_wontwork": "8.5.0",
"current_snipeit_version": "8.0"
"php_min_version": "8.1.0",
"php_max_major_minor": "8.3",
"php_max_wontwork": "8.4.0",
"current_snipeit_version": "7.0"
}

View File

@@ -42,33 +42,17 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/1975640?v=4" width="110px;"/><br /><sub>Evan Taylor</sub>](https://github.com/Delta5)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Delta5 "Code") | [<img src="https://avatars.githubusercontent.com/u/8735148?v=4" width="110px;"/><br /><sub>Petri Asikainen</sub>](https://github.com/PetriAsi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PetriAsi "Code") | [<img src="https://avatars.githubusercontent.com/u/11424540?v=4" width="110px;"/><br /><sub>derdeagle</sub>](https://github.com/derdeagle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derdeagle "Code") | [<img src="https://avatars.githubusercontent.com/u/176950?v=4" width="110px;"/><br /><sub>Mike Frysinger</sub>](https://wh0rd.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vapier "Code") | [<img src="https://avatars.githubusercontent.com/u/22044358?v=4" width="110px;"/><br /><sub>ALPHA</sub>](https://github.com/AL4AL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AL4AL "Code") | [<img src="https://avatars.githubusercontent.com/u/1042587?v=4" width="110px;"/><br /><sub>FliegenKLATSCH</sub>](https://www.ifern.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH "Code") | [<img src="https://avatars.githubusercontent.com/u/442138?v=4" width="110px;"/><br /><sub>Jeremy Price</sub>](https://github.com/jerm)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jerm "Code") |
| [<img src="https://avatars.githubusercontent.com/u/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") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/qveensi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qveensi "Code") | [<img src="https://avatars.githubusercontent.com/u/3839381?v=4" width="110px;"/><br /><sub>Achmad Fienan Rahardianto</sub>](https://github.com/veenone)<br />[💻](https://github.com/snipe/snipe-it/commits?author=veenone "Code") | [<img src="https://avatars.githubusercontent.com/u/97299851?v=4" width="110px;"/><br /><sub>Christian Weirich</sub>](https://github.com/chrisweirich)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chrisweirich "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1294403?v=4" width="110px;"/><br /><sub>denzfarid</sub>](https://github.com/denzfarid)<br /> | [<img src="https://avatars.githubusercontent.com/u/94018771?v=4" width="110px;"/><br /><sub>ntbutler-nbcs</sub>](https://github.com/ntbutler-nbcs)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs "Code") | [<img src="https://avatars.githubusercontent.com/u/172697?v=4" width="110px;"/><br /><sub>Naveen</sub>](https://naveensrinivasan.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=naveensrinivasan "Code") | [<img src="https://avatars.githubusercontent.com/u/55674383?v=4" width="110px;"/><br /><sub>Mike Roquemore</sub>](https://github.com/mikeroq)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikeroq "Code") | [<img src="https://avatars.githubusercontent.com/u/7991086?v=4" width="110px;"/><br /><sub>Daniel Reeder</sub>](https://github.com/reederda)<br />[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=reederda "Code") | [<img src="https://avatars.githubusercontent.com/u/109422491?v=4" width="110px;"/><br /><sub>vickyjaura183</sub>](https://github.com/vickyjaura183)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vickyjaura183 "Code") | [<img src="https://avatars.githubusercontent.com/u/32363424?v=4" width="110px;"/><br /><sub>Peace</sub>](https://github.com/julian-piehl)<br />[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") |
| [<img src="https://avatars.githubusercontent.com/u/231528?v=4" width="110px;"/><br /><sub>Kyle Gordon</sub>](https://github.com/kylegordon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [<img src="https://avatars.githubusercontent.com/u/53009155?v=4" width="110px;"/><br /><sub>Katharina Drexel</sub>](http://www.bfh.ch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [<img src="https://avatars.githubusercontent.com/u/1931963?v=4" width="110px;"/><br /><sub>David Sferruzza</sub>](https://david.sferruzza.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [<img src="https://avatars.githubusercontent.com/u/19511639?v=4" width="110px;"/><br /><sub>Rick Nelson</sub>](https://github.com/rnelsonee)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [<img src="https://avatars.githubusercontent.com/u/94169344?v=4" width="110px;"/><br /><sub>BasO12</sub>](https://github.com/BasO12)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [<img src="https://avatars.githubusercontent.com/u/111710123?v=4" width="110px;"/><br /><sub>Vautia</sub>](https://github.com/Vautia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") | [<img src="https://avatars.githubusercontent.com/u/28321?v=4" width="110px;"/><br /><sub>Chris Hartjes</sub>](http://www.littlehart.net/atthekeyboard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") |
| [<img src="https://avatars.githubusercontent.com/u/2404584?v=4" width="110px;"/><br /><sub>geo-chen</sub>](https://github.com/geo-chen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [<img src="https://avatars.githubusercontent.com/u/6006620?v=4" width="110px;"/><br /><sub>Phan Nguyen</sub>](https://github.com/nh314)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [<img src="https://avatars.githubusercontent.com/u/115993812?v=4" width="110px;"/><br /><sub>Iisakki Jaakkola</sub>](https://github.com/StarlessNights)<br />[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="110px;"/><br /><sub>Ikko Ashimine</sub>](https://bandism.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [<img src="https://avatars.githubusercontent.com/u/56871540?v=4" width="110px;"/><br /><sub>Lukas Fehling</sub>](https://github.com/lukasfehling)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [<img src="https://avatars.githubusercontent.com/u/1975990?v=4" width="110px;"/><br /><sub>Fernando Almeida</sub>](https://github.com/fernando-almeida)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") | [<img src="https://avatars.githubusercontent.com/u/116301219?v=4" width="110px;"/><br /><sub>akemidx</sub>](https://github.com/akemidx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") |
| [<img src="https://avatars.githubusercontent.com/u/144778?v=4" width="110px;"/><br /><sub>Oguz Bilgic</sub>](http://oguz.site)<br />[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [<img src="https://avatars.githubusercontent.com/u/9262438?v=4" width="110px;"/><br /><sub>Scooter Crawford</sub>](https://github.com/scoo73r)<br />[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [<img src="https://avatars.githubusercontent.com/u/5957345?v=4" width="110px;"/><br /><sub>subdriven</sub>](https://github.com/subdriven)<br />[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [<img src="https://avatars.githubusercontent.com/u/658865?v=4" width="110px;"/><br /><sub>Andrew Savinykh</sub>](https://github.com/AndrewSav)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [<img src="https://avatars.githubusercontent.com/u/1155067?v=4" width="110px;"/><br /><sub>Tadayuki Onishi</sub>](https://kenchan0130.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [<img src="https://avatars.githubusercontent.com/u/112496896?v=4" width="110px;"/><br /><sub>Florian</sub>](https://github.com/floschoepfer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") | [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> | [<img src="https://avatars.githubusercontent.com/u/5731963?v=4" width="110px;"/><br /><sub>dboth</sub>](http://dboth.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [<img src="https://avatars.githubusercontent.com/u/87536651?v=4" width="110px;"/><br /><sub>Zachary Fleck</sub>](https://github.com/zacharyfleck)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | [<img src="https://avatars.githubusercontent.com/u/74609912?v=4" width="110px;"/><br /><sub>VIKAAS-A</sub>](https://github.com/vikaas-cyper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vikaas-cyper "Code") | [<img src="https://avatars.githubusercontent.com/u/88882041?v=4" width="110px;"/><br /><sub>Abdul Kareem</sub>](https://github.com/ak-piracha)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ak-piracha "Code") | [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") |
| [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") | [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") |
| [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") | [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") | [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") |
| [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") | [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | [<img src="https://avatars.githubusercontent.com/u/6136439?v=4" width="110px;"/><br /><sub>Darren Rainey</sub>](https://darrenraineys.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [<img src="https://avatars.githubusercontent.com/u/133033121?v=4" width="110px;"/><br /><sub>maciej-poleszczyk</sub>](https://github.com/maciej-poleszczyk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") | [<img src="https://avatars.githubusercontent.com/u/143394709?v=4" width="110px;"/><br /><sub>Sebastian Groß</sub>](https://github.com/sgross-emlix)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sgross-emlix "Code") | [<img src="https://avatars.githubusercontent.com/u/41107778?v=4" width="110px;"/><br /><sub>Anouar Touati</sub>](https://github.com/AnouarTouati)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AnouarTouati "Code") |
| [<img src="https://avatars.githubusercontent.com/u/25596663?v=4" width="110px;"/><br /><sub>aHVzY2g</sub>](https://github.com/aHVzY2g)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aHVzY2g "Code") | [<img src="https://avatars.githubusercontent.com/u/13408130?v=4" width="110px;"/><br /><sub>林博仁 Buo-ren Lin</sub>](https://brlin.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brlin-tw "Code") | [<img src="https://avatars.githubusercontent.com/u/18550946?v=4" width="110px;"/><br /><sub>Adugna Gizaw</sub>](https://orbalia.pythonanywhere.com/)<br />[🌍](#translation-addex12 "Translation") | [<img src="https://avatars.githubusercontent.com/u/760989?v=4" width="110px;"/><br /><sub>Jesse Ostrander</sub>](https://github.com/jostrander)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jostrander "Code") | [<img src="https://avatars.githubusercontent.com/u/31522486?v=4" width="110px;"/><br /><sub>James M</sub>](https://github.com/azmcnutt)<br />[💻](https://github.com/snipe/snipe-it/commits?author=azmcnutt "Code") | [<img src="https://avatars.githubusercontent.com/u/5183146?v=4" width="110px;"/><br /><sub>Fiala06</sub>](https://github.com/Fiala06)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Fiala06 "Code") | [<img src="https://avatars.githubusercontent.com/u/28693782?v=4" width="110px;"/><br /><sub>Nathan Taylor</sub>](https://github.com/ntaylor-86)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntaylor-86 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/16699443?v=4" width="110px;"/><br /><sub>fvollmer</sub>](https://github.com/fvollmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fvollmer "Code") | [<img src="https://avatars.githubusercontent.com/u/109086466?v=4" width="110px;"/><br /><sub>36864</sub>](https://github.com/36864)<br />[💻](https://github.com/snipe/snipe-it/commits?author=36864 "Code") | [<img src="https://avatars.githubusercontent.com/u/365751?v=4" width="110px;"/><br /><sub>Daniel O'Connor</sub>](http://clockwerx.blogspot.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CloCkWeRX "Code") | [<img src="https://avatars.githubusercontent.com/u/102852568?v=4" width="110px;"/><br /><sub>BeatSpark</sub>](https://github.com/BeatSpark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BeatSpark "Code") | [<img src="https://avatars.githubusercontent.com/u/59203607?v=4" width="110px;"/><br /><sub>mrdahbi</sub>](https://github.com/mrdahbi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mrdahbi "Code") | [<img src="https://avatars.githubusercontent.com/u/6661332?v=4" width="110px;"/><br /><sub>Fabian Schmid</sub>](http://sr.solutions)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chfsx "Code") | [<img src="https://avatars.githubusercontent.com/u/1288116?v=4" width="110px;"/><br /><sub>Chris Olin</sub>](https://www.chrisolin.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=realchrisolin "Code") |
| [<img src="https://avatars.githubusercontent.com/u/3803132?v=4" width="110px;"/><br /><sub>Dan</sub>](https://github.com/mnemonicly)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mnemonicly "Code") | [<img src="https://avatars.githubusercontent.com/u/43917728?v=4" width="110px;"/><br /><sub>Nebel</sub>](https://github.com/NebelKreis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NebelKreis "Code") | [<img src="https://avatars.githubusercontent.com/u/132433803?v=4" width="110px;"/><br /><sub>test1337ahp</sub>](https://github.com/test1337ahp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=test1337ahp "Code") | [<img src="https://avatars.githubusercontent.com/u/1916566?v=4" width="110px;"/><br /><sub>Jonathon Reinhart</sub>](https://github.com/JonathonReinhart)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JonathonReinhart "Code") | [<img src="https://avatars.githubusercontent.com/u/484742?v=4" width="110px;"/><br /><sub>aranar-pro</sub>](https://github.com/aranar-pro)<br />[💻](https://github.com/snipe/snipe-it/commits?author=aranar-pro "Code") | [<img src="https://avatars.githubusercontent.com/u/27019397?v=4" width="110px;"/><br /><sub>Phil</sub>](https://github.com/phil-flip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=phil-flip "Code") | [<img src="https://avatars.githubusercontent.com/u/6473460?v=4" width="110px;"/><br /><sub>Steffy Fort</sub>](https://fe80.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fe80 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/3302372?v=4" width="110px;"/><br /><sub>Jared Busch</sub>](https://github.com/sorvani)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sorvani "Code") | [<img src="https://avatars.githubusercontent.com/u/111956991?v=4" width="110px;"/><br /><sub>seanborg-codethink</sub>](https://github.com/seanborg-codethink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanborg-codethink "Code") | [<img src="https://avatars.githubusercontent.com/u/160669961?v=4" width="110px;"/><br /><sub>dkaatz</sub>](https://github.com/dkaatz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dkaatz "Code") | [<img src="https://avatars.githubusercontent.com/u/827205?v=4" width="110px;"/><br /><sub>Daniel Ruf</sub>](https://threema.id/74SF7MW6?text=)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DanielRuf "Code") | [<img src="https://avatars.githubusercontent.com/u/38883201?v=4" width="110px;"/><br /><sub>ahpaleus</sub>](https://github.com/ahpaleus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ahpaleus "Code") | [<img src="https://avatars.githubusercontent.com/u/22906055?v=4" width="110px;"/><br /><sub>Anh DAO-DUY</sub>](https://github.com/mink-adao-duy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mink-adao-duy "Code") | [<img src="https://avatars.githubusercontent.com/u/4723453?v=4" width="110px;"/><br /><sub>Andres Gutierrez</sub>](https://github.com/Serdnad)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Serdnad "Code") |
| [<img src="https://avatars.githubusercontent.com/u/111083379?v=4" width="110px;"/><br /><sub>Warren White</sub>](https://github.com/wewhite)<br />[💻](https://github.com/snipe/snipe-it/commits?author=wewhite "Code") | [<img src="https://avatars.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://robintemme.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=robintemme "Code") | [<img src="https://avatars.githubusercontent.com/u/47008367?v=4" width="110px;"/><br /><sub>herroworrd</sub>](https://github.com/herroworrd)<br />[💻](https://github.com/snipe/snipe-it/commits?author=herroworrd "Code") | [<img src="https://avatars.githubusercontent.com/u/28558609?v=4" width="110px;"/><br /><sub>vicleos</sub>](https://mubiu.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vicleos "Code") | [<img src="https://avatars.githubusercontent.com/u/1016780?v=4" width="110px;"/><br /><sub>Bob Clough</sub>](http://thinkl33t.co.uk/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thinkl33t "Code") | [<img src="https://avatars.githubusercontent.com/u/10648463?v=4" width="110px;"/><br /><sub>Brandon Daniel Bailey</sub>](https://github.com/brandon-bailey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=brandon-bailey "Code") | [<img src="https://avatars.githubusercontent.com/u/23556080?v=4" width="110px;"/><br /><sub>Marc Bartelt</sub>](https://github.com/marcquark)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcquark "Code") |
| [<img src="https://avatars.githubusercontent.com/u/18286893?v=4" width="110px;"/><br /><sub>manu-crealytics</sub>](https://github.com/manu-crealytics)<br />[💻](https://github.com/snipe/snipe-it/commits?author=manu-crealytics "Code") | [<img src="https://avatars.githubusercontent.com/u/18245993?v=4" width="110px;"/><br /><sub>Konstantin Köhring</sub>](https://www.galaxy102.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Galaxy102 "Code") | [<img src="https://avatars.githubusercontent.com/u/685167?v=4" width="110px;"/><br /><sub>Deloz</sub>](https://deloz.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=deloz "Code") | [<img src="https://avatars.githubusercontent.com/u/2682426?v=4" width="110px;"/><br /><sub>Martin Berg</sub>](https://github.com/mbrrg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mbrrg "Code") | [<img src="https://avatars.githubusercontent.com/u/3694534?v=4" width="110px;"/><br /><sub>Richard Schwab</sub>](https://github.com/Nothing4You)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Nothing4You "Code") | [<img src="https://avatars.githubusercontent.com/u/8959676?v=4" width="110px;"/><br /><sub>Rick Heil</sub>](https://rickheil.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rickheil "Code") | [<img src="https://avatars.githubusercontent.com/u/397106?v=4" width="110px;"/><br /><sub>Ross Crawford-d'Heureuse</sub>](https://github.com/rosscdh)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rosscdh "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1621107?v=4" width="110px;"/><br /><sub>Ryan McGuire</sub>](https://github.com/McG800)<br />[💻](https://github.com/snipe/snipe-it/commits?author=McG800 "Code") | [<img src="https://avatars.githubusercontent.com/u/77835667?v=4" width="110px;"/><br /><sub>SBrown2021</sub>](https://github.com/SBrown2021)<br />[💻](https://github.com/snipe/snipe-it/commits?author=SBrown2021 "Code") | [<img src="https://avatars.githubusercontent.com/u/8780913?v=4" width="110px;"/><br /><sub>Serkan</sub>](https://github.com/serkanerip)<br />[💻](https://github.com/snipe/snipe-it/commits?author=serkanerip "Code") | [<img src="https://avatars.githubusercontent.com/u/63188620?v=4" width="110px;"/><br /><sub>Shanks</sub>](https://www.yudelei.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Shankschn "Code") | [<img src="https://avatars.githubusercontent.com/u/198525698?v=4" width="110px;"/><br /><sub>cendai-mis</sub>](https://github.com/cendai-mis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cendai-mis "Code") | [<img src="https://avatars.githubusercontent.com/u/8724583?v=4" width="110px;"/><br /><sub>Shaun McPeck</sub>](https://smcpeck.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smcpeck "Code") | [<img src="https://avatars.githubusercontent.com/u/1378836?v=4" width="110px;"/><br /><sub>Stephen</sub>](https://github.com/snazy2000)<br />[💻](https://github.com/snipe/snipe-it/commits?author=snazy2000 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/4462739?v=4" width="110px;"/><br /><sub>Steven</sub>](http://nevets82.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Nevets82 "Code") | [<img src="https://avatars.githubusercontent.com/u/29017267?v=4" width="110px;"/><br /><sub>Mateus Villar</sub>](https://mateusvillar.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Mateus-Romera "Code") | [<img src="https://avatars.githubusercontent.com/u/12749393?v=4" width="110px;"/><br /><sub>Matthew Zackschewski</sub>](https://github.com/mzack5020)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mzack5020 "Code") | [<img src="https://avatars.githubusercontent.com/u/12660103?v=4" width="110px;"/><br /><sub>Matthias Frei</sub>](https://www.frei.media/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=firefrei "Code") | [<img src="https://avatars.githubusercontent.com/u/824840?v=4" width="110px;"/><br /><sub>Nenad Ticaric</sub>](https://github.com/nticaric)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nticaric "Code") | [<img src="https://avatars.githubusercontent.com/u/706439?v=4" width="110px;"/><br /><sub>Nikolay Didenko</sub>](https://github.com/Scorcher)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scorcher "Code") | [<img src="https://avatars.githubusercontent.com/u/5457236?v=4" width="110px;"/><br /><sub>Nuno Maduro</sub>](https://nunomaduro.com/sponsorships)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nunomaduro "Code") |
| [<img src="https://avatars.githubusercontent.com/u/8883074?v=4" width="110px;"/><br /><sub>Oliver Walerys</sub>](https://tektikhq.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=owalerys "Code") | [<img src="https://avatars.githubusercontent.com/u/3102039?v=4" width="110px;"/><br /><sub>R. Christian McDonald</sub>](https://keybase.io/rcmcdonald91)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rcmcdonald91 "Code") | [<img src="https://avatars.githubusercontent.com/u/1525581?v=4" width="110px;"/><br /><sub>nix</sub>](https://nnix.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nixn "Code") | [<img src="https://avatars.githubusercontent.com/u/55462380?v=4" width="110px;"/><br /><sub>octobunny</sub>](https://github.com/octobunny)<br />[💻](https://github.com/snipe/snipe-it/commits?author=octobunny "Code") | [<img src="https://avatars.githubusercontent.com/u/8558670?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/sreyemnayr)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sreyemnayr "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>p3nj</sub>](https://benji.ltd/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=p3nj "Code") | [<img src="https://avatars.githubusercontent.com/u/6201617?v=4" width="110px;"/><br /><sub>Tim White</sub>](https://github.com/timwsuqld)<br />[💻](https://github.com/snipe/snipe-it/commits?author=timwsuqld "Code") |
| [<img src="https://avatars.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>yannikp</sub>](https://github.com/yannikp)<br />[💻](https://github.com/snipe/snipe-it/commits?author=yannikp "Code") | [<img src="https://avatars.githubusercontent.com/u/20525448?v=4" width="110px;"/><br /><sub>victoria</sub>](https://github.com/viclou)<br />[💻](https://github.com/snipe/snipe-it/commits?author=viclou "Code") | [<img src="https://avatars.githubusercontent.com/u/40685314?v=4" width="110px;"/><br /><sub>Valentyn Tulub</sub>](https://github.com/valentyntu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=valentyntu "Code") | [<img src="https://avatars.githubusercontent.com/u/864520?v=4" width="110px;"/><br /><sub>Wouter van Os</sub>](http://wouter0100.nl/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Wouter0100 "Code") | [<img src="https://avatars.githubusercontent.com/u/3946540?v=4" width="110px;"/><br /><sub>Wyatt Teeter</sub>](https://www.linkedin.com/in/wyatt-teeter)<br />[💻](https://github.com/snipe/snipe-it/commits?author=xWyatt "Code") | [<img src="https://avatars.githubusercontent.com/u/1596124?v=4" width="110px;"/><br /><sub>Yorick Terweijden</sub>](https://github.com/terwey)<br />[💻](https://github.com/snipe/snipe-it/commits?author=terwey "Code") | [<img src="https://avatars.githubusercontent.com/u/69298836?v=4" width="110px;"/><br /><sub>bmkalle</sub>](https://github.com/bmkalle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bmkalle "Code") |
| [<img src="https://avatars.githubusercontent.com/u/28403467?v=4" width="110px;"/><br /><sub>bricelabelle</sub>](https://github.com/bricelabelle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bricelabelle "Code") | [<img src="https://avatars.githubusercontent.com/u/97770090?v=4" width="110px;"/><br /><sub>corydlamb</sub>](https://github.com/corydlamb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=corydlamb "Code") | [<img src="https://avatars.githubusercontent.com/u/1154133?v=4" width="110px;"/><br /><sub>Diogenes S. Jesus</sub>](http://twitter.com/splash)<br />[💻](https://github.com/snipe/snipe-it/commits?author=splashx "Code") | [<img src="https://avatars.githubusercontent.com/u/5826629?v=4" width="110px;"/><br /><sub>D M</sub>](https://github.com/dkmansion)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dkmansion "Code") | [<img src="https://avatars.githubusercontent.com/u/14837699?v=4" width="110px;"/><br /><sub>Dustin B</sub>](https://github.com/Jarli01)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Jarli01 "Code") | [<img src="https://avatars.githubusercontent.com/u/348344?v=4" width="110px;"/><br /><sub>Fabian Grutschus</sub>](https://github.com/fabiang)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fabiang "Code") | [<img src="https://avatars.githubusercontent.com/u/1491053?v=4" width="110px;"/><br /><sub>MelonSmasher</sub>](https://github.com/MelonSmasher)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MelonSmasher "Code") |
| [<img src="https://avatars.githubusercontent.com/u/80526133?v=4" width="110px;"/><br /><sub>AlexanderWPapyrus</sub>](https://github.com/AlexanderWPapyrus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AlexanderWPapyrus "Code") | [<img src="https://avatars.githubusercontent.com/u/306231?v=4" width="110px;"/><br /><sub>Alexandr Hacicheant</sub>](https://github.com/disc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=disc "Code") | [<img src="https://avatars.githubusercontent.com/u/3032891?v=4" width="110px;"/><br /><sub>Hex</sub>](https://hex128.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hex128 "Code") | [<img src="https://avatars.githubusercontent.com/u/8697942?v=4" width="110px;"/><br /><sub>Arunas Skirius</sub>](https://github.com/arukompas)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arukompas "Code") | [<img src="https://avatars.githubusercontent.com/u/104396?v=4" width="110px;"/><br /><sub>Ben Periton</sub>](https://github.com/benperiton)<br />[💻](https://github.com/snipe/snipe-it/commits?author=benperiton "Code") | [<img src="https://avatars.githubusercontent.com/u/11906832?v=4" width="110px;"/><br /><sub>Byron Wolfman</sub>](https://wolfman.dev/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=byronwolfman "Code") | [<img src="https://avatars.githubusercontent.com/u/56485508?v=4" width="110px;"/><br /><sub>Calvin</sub>](https://github.com/CalvinSchwartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CalvinSchwartz "Code") |
| [<img src="https://avatars.githubusercontent.com/u/181059?v=4" width="110px;"/><br /><sub>Juan Font</sub>](https://github.com/juanfont)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juanfont "Code") | [<img src="https://avatars.githubusercontent.com/u/13137708?v=4" width="110px;"/><br /><sub>Juho Taipale</sub>](https://github.com/juhotaipale)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juhotaipale "Code") | [<img src="https://avatars.githubusercontent.com/u/1007419?v=4" width="110px;"/><br /><sub>Korvin Szanto</sub>](https://github.com/KorvinSzanto)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KorvinSzanto "Code") | [<img src="https://avatars.githubusercontent.com/u/8513053?v=4" width="110px;"/><br /><sub>Lewis Foster</sub>](https://lewisfoster.foo/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sniff122 "Code") | [<img src="https://avatars.githubusercontent.com/u/33877541?v=4" width="110px;"/><br /><sub>Logan Swartzendruber</sub>](https://github.com/loganswartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=loganswartz "Code") | [<img src="https://avatars.githubusercontent.com/u/1156208?v=4" width="110px;"/><br /><sub>Lorenzo P.</sub>](https://github.com/lopezio)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lopezio "Code") | [<img src="https://avatars.githubusercontent.com/u/33946590?v=4" width="110px;"/><br /><sub>Lukas Jung</sub>](https://github.com/m4us1ne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=m4us1ne "Code") |
| [<img src="https://avatars.githubusercontent.com/u/10965027?v=4" width="110px;"/><br /><sub>Ellie</sub>](https://leafedfox.xyz/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [<img src="https://avatars.githubusercontent.com/u/20960555?v=4" width="110px;"/><br /><sub>GA Stamper</sub>](https://github.com/gastamper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [<img src="https://avatars.githubusercontent.com/u/206553556?v=4" width="110px;"/><br /><sub>Guillaume Lefranc</sub>](https://github.com/gl-pup)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [<img src="https://avatars.githubusercontent.com/u/733892?v=4" width="110px;"/><br /><sub>Hajo Möller</sub>](https://github.com/dasjoe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [<img src="https://avatars.githubusercontent.com/u/3420063?v=4" width="110px;"/><br /><sub>Istvan Basa</sub>](https://github.com/pottom)<br />[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [<img src="https://avatars.githubusercontent.com/u/810824?v=4" width="110px;"/><br /><sub>JJ Asghar</sub>](https://jjasghar.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [<img src="https://avatars.githubusercontent.com/u/40404495?v=4" width="110px;"/><br /><sub>James E. Msenga</sub>](https://github.com/JemCdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") |
| [<img src="https://avatars.githubusercontent.com/u/38761237?v=4" width="110px;"/><br /><sub>Alex Janes</sub>](https://adagiohealth.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adagioajanes "Code") | [<img src="https://avatars.githubusercontent.com/u/32387849?v=4" width="110px;"/><br /><sub>Nuraeil</sub>](https://github.com/nuraeil)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nuraeil "Code") | [<img src="https://avatars.githubusercontent.com/u/48162670?v=4" width="110px;"/><br /><sub>TenOfTens</sub>](https://github.com/TenOfTens)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TenOfTens "Code") | [<img src="https://avatars.githubusercontent.com/u/9415391?v=4" width="110px;"/><br /><sub>waffle</sub>](https://ditisjens.be/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=insert-waffle "Code") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/QveenSi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=QveenSi "Code") | [<img src="https://avatars.githubusercontent.com/u/3839381?v=4" width="110px;"/><br /><sub>Achmad Fienan Rahardianto</sub>](https://github.com/veenone)<br />[💻](https://github.com/snipe/snipe-it/commits?author=veenone "Code") | [<img src="https://avatars.githubusercontent.com/u/19945501?v=4" width="110px;"/><br /><sub>Yevhenii Huzii</sub>](https://github.com/QveenSi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=QveenSi "Code") |
| [<img src="https://avatars.githubusercontent.com/u/97299851?v=4" width="110px;"/><br /><sub>Christian Weirich</sub>](https://github.com/chrisweirich)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chrisweirich "Code") | [<img src="https://avatars.githubusercontent.com/u/1294403?v=4" width="110px;"/><br /><sub>denzfarid</sub>](https://github.com/denzfarid)<br /> | [<img src="https://avatars.githubusercontent.com/u/94018771?v=4" width="110px;"/><br /><sub>ntbutler-nbcs</sub>](https://github.com/ntbutler-nbcs)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs "Code") | [<img src="https://avatars.githubusercontent.com/u/172697?v=4" width="110px;"/><br /><sub>Naveen</sub>](https://naveensrinivasan.dev)<br />[💻](https://github.com/snipe/snipe-it/commits?author=naveensrinivasan "Code") | [<img src="https://avatars.githubusercontent.com/u/55674383?v=4" width="110px;"/><br /><sub>Mike Roquemore</sub>](https://github.com/mikeroq)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikeroq "Code") | [<img src="https://avatars.githubusercontent.com/u/7991086?v=4" width="110px;"/><br /><sub>Daniel Reeder</sub>](https://github.com/reederda)<br />[🌍](#translation-reederda "Translation") [🌍](#translation-reederda "Translation") [💻](https://github.com/snipe/snipe-it/commits?author=reederda "Code") | [<img src="https://avatars.githubusercontent.com/u/109422491?v=4" width="110px;"/><br /><sub>vickyjaura183</sub>](https://github.com/vickyjaura183)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vickyjaura183 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/32363424?v=4" width="110px;"/><br /><sub>Peace</sub>](https://github.com/julian-piehl)<br />[💻](https://github.com/snipe/snipe-it/commits?author=julian-piehl "Code") | [<img src="https://avatars.githubusercontent.com/u/231528?v=4" width="110px;"/><br /><sub>Kyle Gordon</sub>](https://github.com/kylegordon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kylegordon "Code") | [<img src="https://avatars.githubusercontent.com/u/53009155?v=4" width="110px;"/><br /><sub>Katharina Drexel</sub>](http://www.bfh.ch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sunflowerbofh "Code") | [<img src="https://avatars.githubusercontent.com/u/1931963?v=4" width="110px;"/><br /><sub>David Sferruzza</sub>](https://david.sferruzza.fr/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dsferruzza "Code") | [<img src="https://avatars.githubusercontent.com/u/19511639?v=4" width="110px;"/><br /><sub>Rick Nelson</sub>](https://github.com/rnelsonee)<br />[💻](https://github.com/snipe/snipe-it/commits?author=rnelsonee "Code") | [<img src="https://avatars.githubusercontent.com/u/94169344?v=4" width="110px;"/><br /><sub>BasO12</sub>](https://github.com/BasO12)<br />[💻](https://github.com/snipe/snipe-it/commits?author=BasO12 "Code") | [<img src="https://avatars.githubusercontent.com/u/111710123?v=4" width="110px;"/><br /><sub>Vautia</sub>](https://github.com/Vautia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Vautia "Code") |
| [<img src="https://avatars.githubusercontent.com/u/28321?v=4" width="110px;"/><br /><sub>Chris Hartjes</sub>](http://www.littlehart.net/atthekeyboard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") | [<img src="https://avatars.githubusercontent.com/u/2404584?v=4" width="110px;"/><br /><sub>geo-chen</sub>](https://github.com/geo-chen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [<img src="https://avatars.githubusercontent.com/u/6006620?v=4" width="110px;"/><br /><sub>Phan Nguyen</sub>](https://github.com/nh314)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [<img src="https://avatars.githubusercontent.com/u/115993812?v=4" width="110px;"/><br /><sub>Iisakki Jaakkola</sub>](https://github.com/StarlessNights)<br />[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="110px;"/><br /><sub>Ikko Ashimine</sub>](https://bandism.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [<img src="https://avatars.githubusercontent.com/u/56871540?v=4" width="110px;"/><br /><sub>Lukas Fehling</sub>](https://github.com/lukasfehling)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [<img src="https://avatars.githubusercontent.com/u/1975990?v=4" width="110px;"/><br /><sub>Fernando Almeida</sub>](https://github.com/fernando-almeida)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") |
| [<img src="https://avatars.githubusercontent.com/u/116301219?v=4" width="110px;"/><br /><sub>akemidx</sub>](https://github.com/akemidx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") | [<img src="https://avatars.githubusercontent.com/u/144778?v=4" width="110px;"/><br /><sub>Oguz Bilgic</sub>](http://oguz.site)<br />[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [<img src="https://avatars.githubusercontent.com/u/9262438?v=4" width="110px;"/><br /><sub>Scooter Crawford</sub>](https://github.com/scoo73r)<br />[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [<img src="https://avatars.githubusercontent.com/u/5957345?v=4" width="110px;"/><br /><sub>subdriven</sub>](https://github.com/subdriven)<br />[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [<img src="https://avatars.githubusercontent.com/u/658865?v=4" width="110px;"/><br /><sub>Andrew Savinykh</sub>](https://github.com/AndrewSav)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [<img src="https://avatars.githubusercontent.com/u/1155067?v=4" width="110px;"/><br /><sub>Tadayuki Onishi</sub>](https://kenchan0130.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [<img src="https://avatars.githubusercontent.com/u/112496896?v=4" width="110px;"/><br /><sub>Florian</sub>](https://github.com/floschoepfer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") |
| [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") | [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> | [<img src="https://avatars.githubusercontent.com/u/5731963?v=4" width="110px;"/><br /><sub>dboth</sub>](http://dboth.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [<img src="https://avatars.githubusercontent.com/u/87536651?v=4" width="110px;"/><br /><sub>Zachary Fleck</sub>](https://github.com/zacharyfleck)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | [<img src="https://avatars.githubusercontent.com/u/74609912?v=4" width="110px;"/><br /><sub>VIKAAS-A</sub>](https://github.com/vikaas-cyper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vikaas-cyper "Code") | [<img src="https://avatars.githubusercontent.com/u/88882041?v=4" width="110px;"/><br /><sub>Abdul Kareem</sub>](https://github.com/ak-piracha)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ak-piracha "Code") |
| [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") | [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") | [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") |
| [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") | [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") |
| [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") | [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") |
| [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") | [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") | [<img src="https://avatars.githubusercontent.com/u/6136439?v=4" width="110px;"/><br /><sub>Darren Rainey</sub>](https://darrenraineys.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DarrenRainey "Code") | [<img src="https://avatars.githubusercontent.com/u/133033121?v=4" width="110px;"/><br /><sub>maciej-poleszczyk</sub>](https://github.com/maciej-poleszczyk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=maciej-poleszczyk "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -1,8 +1,8 @@
FROM ubuntu:24.04
FROM ubuntu:22.04
LABEL maintainer="Brady Wetherington <bwetherington@grokability.com>"
# No need to add `apt-get clean` here, reference:
# - https://github.com/grokability/snipe-it/pull/9201
# - https://github.com/snipe/snipe-it/pull/9201
# - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#apt-get
RUN export DEBIAN_FRONTEND=noninteractive; \
@@ -14,16 +14,16 @@ RUN export DEBIAN_FRONTEND=noninteractive; \
apt-utils \
apache2 \
apache2-bin \
libapache2-mod-php8.3 \
php8.3-curl \
php8.3-ldap \
php8.3-mysql \
php8.3-gd \
php8.3-xml \
php8.3-mbstring \
php8.3-zip \
php8.3-bcmath \
php8.3-redis \
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 \
patch \
curl \
@@ -40,7 +40,8 @@ autoconf \
libc-dev \
libldap-common \
pkg-config \
php8.3-dev \
libmcrypt-dev \
php8.1-dev \
ca-certificates \
unzip \
dnsutils \
@@ -50,13 +51,18 @@ 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 bash -c "echo extension=/usr/lib/php/20210902/mcrypt.so > /etc/php/8.1/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.3/apache2/php.ini
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.3/cli/php.ini
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 useradd -m --uid 10000 --gid 50 docker
RUN useradd -m --uid 1000 --gid 50 docker
RUN echo export APACHE_RUN_USER=docker >> /etc/apache2/envvars
RUN echo export APACHE_RUN_GROUP=staff >> /etc/apache2/envvars
@@ -110,7 +116,7 @@ COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Get dependencies
USER docker
RUN COMPOSER_CACHE_DIR=/dev/null composer install --no-dev --working-dir=/var/www/html && rm -rf /var/www/html/vendor/*/*/.git
RUN composer install --no-dev --working-dir=/var/www/html
USER root
############### APPLICATION INSTALL/INIT #################

View File

@@ -73,7 +73,7 @@ RUN mkdir -p /var/www/.composer && chown apache /var/www/.composer
# Install dependencies
USER apache
RUN COMPOSER_CACHE_DIR=/dev/null composer install --working-dir=/var/www/html
RUN COMPOSER_CACHE_DIR=/dev/null composer install --no-dev --working-dir=/var/www/html
USER root

View File

@@ -70,7 +70,7 @@ COPY --from=composer /usr/bin/composer /usr/local/bin
ARG COMPOSER_ALLOW_SUPERUSER=1
RUN set -eux; \
# Download and extract snipeit tarball
curl -o snipeit.tar.gz -fL "https://github.com/grokability/snipe-it/archive/v$SNIPEIT_RELEASE.tar.gz"; \
curl -o snipeit.tar.gz -fL "https://github.com/snipe/snipe-it/archive/v$SNIPEIT_RELEASE.tar.gz"; \
tar -xzf snipeit.tar.gz --strip-components=1 -C /var/www/html/; \
rm snipeit.tar.gz; \
# Install composer php dependencies

View File

@@ -1,15 +1,15 @@
![snipe-it-by-grok](https://github.com/grokability/snipe-it/assets/197404/b515673b-c7c8-4d9a-80f5-9fa58829a602)
![snipe-it-by-grok](https://github.com/snipe/snipe-it/assets/197404/b515673b-c7c8-4d9a-80f5-9fa58829a602)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/804dd1beb14a41f38810ab77d64fc4fc)](https://app.codacy.com/gh/grokability/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests](https://github.com/grokability/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/grokability/snipe-it/actions/workflows/tests.yml)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://app.codacy.com/gh/snipe/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests](https://github.com/snipe/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/snipe/snipe-it/actions/workflows/tests.yml)
[![All Contributors](https://img.shields.io/badge/all_contributors-331-orange.svg?style=flat-square)](#contributing) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk)
## 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 11](http://laravel.com).
It is built on [Laravel 10](http://laravel.com).
Snipe-IT is actively developed and we [release quite frequently](https://github.com/grokability/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).)
Snipe-IT is actively developed and we [release quite frequently](https://github.com/snipe/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).)
> [!TIP]
> __This is web-based software__. This means there is no executable file (aka no .exe files), and it must be run on a web server and accessed through a web browser. It runs on any Mac OSX, any flavor of Linux, as well as Windows, and we have a [Docker image](https://snipe-it.readme.io/docs/docker) available if that's what you're into.
@@ -44,7 +44,7 @@ For help using Snipe-IT, check out the [user's manual](https://snipe-it.readme.i
-----
### Bug Reports & Feature Requests
Feel free to check out the [GitHub Issues for this project](https://github.com/grokability/snipe-it/issues) to open a bug report or see what open issues you can help with. Please search through existing issues (open *and* closed) to see if your question has already been answered before opening a new issue.
Feel free to check out the [GitHub Issues for this project](https://github.com/snipe/snipe-it/issues) to open a bug report or see what open issues you can help with. Please search through existing issues (open *and* closed) to see if your question has already been answered before opening a new issue.
> [!IMPORTANT]
> **PLEASE see the [Getting Help Guidelines](https://snipe-it.readme.io/docs/getting-help) and [Common Issues](https://snipe-it.readme.io/docs/common-issues) before opening a ticket, and be sure to complete all of the questions in the Github Issue template to help us to help you as quickly as possible.**
@@ -76,36 +76,23 @@ Since the release of the JSON REST API, several third-party developers have been
> [!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. :)
#### Libraries & Modules
- [Python Module](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
- [SnipeSharp - .NET module in C#](https://github.com/barrycarey/SnipeSharp) by [@barrycarey](https://github.com/barrycarey)
- [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
- [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://www.edtechirl.com/p/snipe-it-and-azure-asset-management) originally by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT.
- [UniFi to Snipe-IT](https://github.com/RodneyLeeBrands/UnifiSnipeSync) by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT.
- [Kandji2Snipe](https://github.com/grokability/kandji2snipe) by [@briangoldstein](https://github.com/briangoldstein) - Python script that synchronizes Kandji with Snipe-IT.
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by [@ReticentRobot](https://github.com/ReticentRobot) - Windows agent for Snipe-IT.
- [Gate Pass Generator](https://github.com/cha7uraAE/snipe-it-gate-pass-system) by [@cha7uraAE](https://github.com/cha7uraAE) - A Streamlit application for generating gate passes based on hardware data from a Snipe-IT API.
- [InQRy (archived)](https://github.com/Microsoft/InQRy) by [@Microsoft](https://github.com/Microsoft)
- [Marksman (archived)](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
- [Python Module (archived)](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
We also have a handful of [Google Apps scripts](https://github.com/grokability/google-apps-scripts-for-snipe-it) to help with various tasks.
#### Mobile Apps
We're currently working on our own mobile app, but in the meantime, check out these third-party apps that work with Snipe-IT:
- [SnipeMate](https://snipemate.app/) (iOS, Google Play, Huawei AppGallery) by Mars Technology
- [Snipe-Scan](https://apps.apple.com/do/app/snipe-scan/id6744179400?uo=2) (iOS) by Nicolas Maton
- [Snipe-IT Assets Management](https://play.google.com/store/apps/details?id=com.diegogarciadev.assetsmanager.snipeit&hl=en&pli=1) (Google Play) by DiegoGarciaDEV
- [AssetX](https://apps.apple.com/my/app/assetx-for-snipe-it/id6741996196?uo=2) (iOS) for Snipe-IT by Rishi Gupta
-----
@@ -133,15 +120,9 @@ The ERD is available [online here](https://drawsql.app/templates/snipe-it).
Be sure to check out all of the [amazing people](CONTRIBUTORS.md) that have contributed to Snipe-IT over the years!
-----
### Star History
[![Star History Chart](https://api.star-history.com/svg?repos=grokability/snipe-it&type=Date)](https://www.star-history.com/#grokability/snipe-it&Date)
------
### Announcement List
To be notified of important news (such as new releases, security advisories, etc), [sign up for our list](http://eepurl.com/XyZKz). We'll never sell or give away your info, and we'll only email you when it's important.
To be notified of important news (such as new releases, security advisories, etc), [sign up for our list](http://eepurl.com/XyZKz). We'll never sell or give away your info, and we'll only email you when it's important.
We also usually make smaller announcements on our social accounts, our Discord, and our blog, so be sure to subscribe to those if you're looking for more granular announcements.

View File

@@ -11,7 +11,6 @@ make it impossible to backport security fixes on older versions.
| Version | Supported |
|---------| ------------------ |
| 8.x | :white_check_mark: |
| 7.x | :white_check_mark: |
| 6.x | :x: |
| 5.1.x | :x: |

2
Vagrantfile vendored
View File

@@ -1,7 +1,7 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
SNIPEIT_SH_URL= "https://raw.githubusercontent.com/grokability/snipe-it/master/snipeit.sh"
SNIPEIT_SH_URL= "https://raw.githubusercontent.com/snipe/snipe-it/master/snipeit.sh"
NETWORK_BRIDGE= "en0: Wi-Fi (AirPort)"
Vagrant.configure("2") do |config|

View File

@@ -6,7 +6,7 @@
"it asset"
],
"website": "https://snipeitapp.com/",
"repository": "https://github.com/grokability/snipe-it",
"repository": "https://github.com/snipe/snipe-it",
"logo": "https://pbs.twimg.com/profile_images/976748875733020672/K-HnZCCK_400x400.jpg",
"success_url": "/setup",
"env": {

View File

@@ -1,48 +0,0 @@
<?php
namespace App\Actions\CheckoutRequests;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\RequestAssetCancelation;
use Illuminate\Auth\Access\AuthorizationException;
class CancelCheckoutRequestAction
{
public static function run(Asset $asset, User $user)
{
if (!Company::isCurrentUserHasAccess($asset)) {
throw new AuthorizationException();
}
$asset->cancelRequest();
$asset->decrement('requests_counter', 1);
$data['item'] = $asset;
$data['target'] = $user;
$data['item_quantity'] = 1;
$settings = Setting::getSettings();
$logaction = new Actionlog();
$logaction->item_id = $data['asset_id'] = $asset->id;
$logaction->item_type = $data['item_type'] = Asset::class;
$logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s');
$logaction->target_id = $data['user_id'] = auth()->id();
$logaction->target_type = User::class;
$logaction->location_id = $user->location_id ?? null;
$logaction->logaction('request canceled');
try {
$settings->notify(new RequestAssetCancelation($data));
} catch (\Exception $e) {
\Log::warning($e);
}
return true;
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Actions\CheckoutRequests;
use App\Exceptions\AssetNotRequestable;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\RequestAssetNotification;
use Illuminate\Auth\Access\AuthorizationException;
use Log;
class CreateCheckoutRequestAction
{
/**
* @throws AssetNotRequestable
* @throws AuthorizationException
*/
public static function run(Asset $asset, User $user): string
{
if (is_null(Asset::RequestableAssets()->find($asset->id))) {
throw new AssetNotRequestable($asset);
}
if (!Company::isCurrentUserHasAccess($asset)) {
throw new AuthorizationException();
}
$data['item'] = $asset;
$data['target'] = $user;
$data['item_quantity'] = 1;
$settings = Setting::getSettings();
$logaction = new Actionlog();
$logaction->item_id = $data['asset_id'] = $asset->id;
$logaction->item_type = $data['item_type'] = Asset::class;
$logaction->created_at = $data['requested_date'] = date('Y-m-d H:i:s');
$logaction->target_id = $data['user_id'] = auth()->id();
$logaction->target_type = User::class;
$logaction->location_id = $user->location_id ?? null;
$logaction->logaction('requested');
$asset->request();
$asset->increment('requests_counter', 1);
try {
$settings->notify(new RequestAssetNotification($data));
} catch (\Exception $e) {
Log::warning($e);
}
return true;
}
}

View File

@@ -1,68 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Console\Command;
class CleanIncorrectCheckoutAcceptances extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:clean-checkout-acceptances';
/**
* The console command description.
*
* @var string
*/
protected $description = "Delete checkout acceptances for checkouts to non-users";
/**
* Execute the console command.
*/
public function handle()
{
$deletions = 0;
$skips = 0;
// This walks *every* checkoutacceptance. That's gnarly. But necessary
$this->withProgressBar(CheckoutAcceptance::all(), function ($checkoutAcceptance) use (&$deletions, &$skips) {
$item = $checkoutAcceptance->checkoutable;
$checkout_to_id = $checkoutAcceptance->assigned_to_id;
if(is_null($item)) {
$this->info("'Checkoutable' Item is null, going to next record");
return; //'false' allegedly breaks execution entirely, so 'true' maybe doesn't? hrm. just straight return maybe?
}
if(get_class($item) == LicenseSeat::class) {
$item = $item->license;
}
foreach($item->assetlog()->where('action_type','checkout')->get() as $assetlog) {
if ($assetlog->target_id == $checkout_to_id && $assetlog->target_type != User::class) {
//We have a checkout-to an ID for a non-User, which matches to an ID in the checkout_acceptances table
//now, let's compare the _times_ - are they close?
//I'm picking `created_at` over `action_date` because I'm more interested in when the actionlogs
//were _created_, not when they were alleged to have happened - those created_at times need to be within 'X' seconds of
//each other (currently 5)
if ($assetlog->created_at->diffInSeconds($checkoutAcceptance->created_at, true) <= 5) { //we're allowing for five _ish_ seconds of slop
$deletions++;
$checkoutAcceptance->forceDelete(); // HARD delete this record; it should have never been
return;
} else {
//$this->info("The two records are too far apart");
}
} else {
//$this->info("No match! checkout to id: " . $checkout_to_id." target_id: ".$assetlog->target_id." target_type: ".$assetlog->target_type);
}
}
$skips++;
});
$this->error("Final deletion count: $deletions, and skip count: $skips");
}
}

View File

@@ -1,74 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\CheckoutRequest;
use Illuminate\Console\Command;
class CleanOldCheckoutRequests extends Command
{
private int $deletions = 0;
private int $skips = 0;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:clean-old-checkout-requests';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Removes checkout requests that reference deleted assets or users.';
/**
* Execute the console command.
*/
public function handle()
{
$requests = CheckoutRequest::with([
'user' => function ($query) {
$query->withTrashed();
},
'requestedItem' => function ($query) {
$query->withTrashed();
},
])->get();
$this->info("Processing {$requests->count()} checkout requests");
$this->withProgressBar($requests, function ($request) {
if ($this->shouldForceDelete($request)) {
$request->forceDelete();
$this->deletions++;
return;
}
if ($this->shouldSoftDelete($request)) {
$request->delete();
$this->deletions++;
return;
}
$this->skips++;
});
$this->info("Final deletion count: $this->deletions, and skip count: $this->skips");
return 0;
}
private function shouldForceDelete(CheckoutRequest $request)
{
// check if the requestable or user relationship is null
return !$request->requestable || !$request->user;
}
private function shouldSoftDelete(CheckoutRequest $request)
{
return $request->requestable->trashed() || $request->user->trashed();
}
}

View File

@@ -1,53 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Setting;
use Illuminate\Console\Command;
class DisableSAML extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:saml-disable';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This is a rescue command that can be used to turn off SAML settings in the event that you managed to lock yourself out using bad SAML settings.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->confirm("\n****************************************************\nThis will disable SAML support. You will not be able \nto login with an account that does not exist \nlocally in the Snipe-IT local database. \n****************************************************\n\nDo you wish to continue? [y|N]")) {
$setting = Setting::getSettings();
$setting->saml_enabled = 0;
if ($setting->save()) {
$this->info('SAML has been set to disabled.');
} else {
$this->info('Unable to disable SAML.');
}
} else {
$this->info('Canceled. No actions taken.');
}
}
}

View File

@@ -1,151 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Accessory;
use App\Models\Actionlog;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
class FixBulkAccessoryCheckinActionLogEntries extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:fix-bulk-accessory-action-log-entries {--dry-run : Run the sync process but don\'t update the database} {--skip-backup : Skip pre-execution backup}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This script attempts to fix timestamps and missing created_by values for bulk checkin entries in the log table';
private bool $dryrun = false;
private bool $skipBackup = false;
/**
* Execute the console command.
*/
public function handle()
{
$this->skipBackup = $this->option('skip-backup');
$this->dryrun = $this->option('dry-run');
if ($this->dryrun) {
$this->info('This is a DRY RUN - no changes will be saved.');
$this->newLine();
}
$logs = Actionlog::query()
// only look for accessory checkin logs
->where('item_type', Accessory::class)
// that were part of a bulk checkin
->where('note', 'Bulk checkin items')
// logs that were improperly timestamped should have created_at in the 1970s
->whereYear('created_at', '1970')
->get();
if ($logs->isEmpty()) {
$this->info('No logs found with incorrect timestamps.');
return 0;
}
$this->info('Found ' . $logs->count() . ' logs with incorrect timestamps:');
$this->table(
['ID', 'Created By', 'Created At', 'Updated At'],
$logs->map(function ($log) {
return [
$log->id,
$log->created_by,
$log->created_at,
$log->updated_at,
];
})
);
if (!$this->dryrun && !$this->confirm('Update these logs?')) {
return 0;
}
if (!$this->dryrun && !$this->skipBackup) {
$this->info('Backing up the database before making changes...');
$this->call('snipeit:backup');
}
if ($this->dryrun) {
$this->newLine();
$this->info('DRY RUN. NOT ACTUALLY UPDATING LOGS.');
}
foreach ($logs as $log) {
$this->newLine();
$this->info('Processing log id:' . $log->id);
// created_by was not being set for accessory bulk checkins
// so let's see if there was another bulk checkin log
// with the same timestamp and a created_by value we can use.
if (is_null($log->created_by)) {
$createdByFromSimilarLog = $this->getCreatedByAttributeFromSimilarLog($log);
if ($createdByFromSimilarLog) {
$this->line(vsprintf('Updating log id:%s created_by to %s', [$log->id, $createdByFromSimilarLog]));
$log->created_by = $createdByFromSimilarLog;
} else {
$this->warn(vsprintf('No created_by found for log id:%s', [$log->id]));
$this->warn('Skipping updating this log since no similar log was found to update created_by from.');
// If we can't find a similar log then let's skip updating it
continue;
}
}
$this->line(vsprintf('Updating log id:%s from %s to %s', [$log->id, $log->created_at, $log->updated_at]));
$log->created_at = $log->updated_at;
if (!$this->dryrun) {
Model::withoutTimestamps(function () use ($log) {
$log->saveQuietly();
});
}
}
$this->newLine();
if ($this->dryrun) {
$this->info('DRY RUN. NO CHANGES WERE ACTUALLY MADE.');
}
return 0;
}
/**
* Hopefully the bulk checkin included other items like assets or licenses
* so we can use one of those logs to get the correct created_by value.
*
* This method attempts to find a bulk check in log that was
* created at the same time as the log passed in.
*/
private function getCreatedByAttributeFromSimilarLog(Actionlog $log): null|int
{
$similarLog = Actionlog::query()
->whereNotNull('created_by')
->where([
'action_type' => 'checkin from',
'note' => 'Bulk checkin items',
'target_id' => $log->target_id,
'target_type' => $log->target_type,
'created_at' => $log->updated_at,
])
->first();
if ($similarLog) {
return $similarLog->created_by;
}
return null;
}
}

View File

@@ -1,32 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class FixUpAssignedTypeWithoutAssignedTo extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:assigned-type-fixup';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Fixes up assets that have an assigned_type but no assigned_to';
/**
* Execute the console command.
*/
public function handle()
{
DB::table('assets')->whereNotNull('assigned_type')->whereNull('assigned_to')->update(['assigned_type' => null]);
$this->info("Assets with an assigned_type but no assigned_to are fixed");
}
}

View File

@@ -2,9 +2,11 @@
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 Illuminate\Support\Facades\DB;
class GeneratePersonalAccessToken extends Command
@@ -41,8 +43,9 @@ class GeneratePersonalAccessToken extends Command
*
* @return void
*/
public function __construct(TokenRepository $tokenRepository)
public function __construct(TokenRepository $tokenRepository, ValidationFactory $validation)
{
$this->validation = $validation;
$this->tokenRepository = $tokenRepository;
parent::__construct();
}
@@ -73,7 +76,7 @@ class GeneratePersonalAccessToken extends Command
} else {
$this->warn('Your API Token has been created. Be sure to copy this token now, as it WILL NOT be accessible again.');
$this->warn('Your API Token has been created. Be sure to copy this token now, as it will not be accessible again.');
if ($token = DB::table('oauth_access_tokens')->where('user_id', '=', $user->id)->where('name','=',$accessTokenName)->orderBy('created_at', 'desc')->first()) {
$this->info('API Token ID: '.$token->id);

View File

@@ -125,10 +125,6 @@ class LdapSync extends Command
*/
$attributes = array_values(array_filter($ldap_map));
if (Setting::getSettings()->is_ad === 1 && is_null($ldap_map['active_flag'])) {
$attributes[] = 'useraccountcontrol';
}
$results = Ldap::findLdapUsers($search_base, -1, $filter, $attributes);
} catch (\Exception $e) {
@@ -182,7 +178,7 @@ class LdapSync extends Command
// Inject location information fields
for ($i = 0; $i < $results['count']; $i++) {
$results[$i]['ldap_location_override'] = false;
$results[$i]['location_id'] = null;
$results[$i]['location_id'] = 0;
}
// Grab subsets based on location-specific DNs, and overwrite location for these users.
@@ -361,15 +357,9 @@ class LdapSync extends Command
// (Specifically, we don't handle a value of '0.0' correctly)
$raw_value = @$results[$i][$ldap_map["active_flag"]][0];
$filter_var = filter_var($raw_value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$boolean_cast = (bool) $raw_value;
if (Setting::getSettings()->ldap_invert_active_flag === 1) {
// Because ldap_active_flag is set, if filter_var is true or boolean_cast is true, then user is suspended
$user->activated = !($filter_var ?? $boolean_cast);
}else{
$user->activated = $filter_var ?? $boolean_cast; // if filter_var() was true or false, use that. If it's null, use the $boolean_cast
}
$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,
@@ -434,20 +424,10 @@ class LdapSync extends Command
$item['note'] = $item['createorupdate'];
$item['status'] = 'success';
if ($item['createorupdate'] === 'created' && $ldap_default_group) {
// Check if the relationship already exists
if (!$user->groups()->where('group_id', $ldap_default_group)->exists()) {
$user->groups()->attach($ldap_default_group);
}
$user->groups()->attach($ldap_default_group);
}
//updates assets location based on user's location
if ($user->wasChanged('location_id')) {
foreach ($user->assets as $asset) {
$asset->location_id = $user->location_id;
// TODO: somehow add note? "Asset Location Changed because of thing"
$asset->save();
}
}
Asset::where('assigned_to', '=', $user->id)->where('assigned_type', '=', User::class)->update(['location_id' => $user->location_id]);
} else {
foreach ($user->getErrors()->getMessages() as $key => $err) {

View File

@@ -96,7 +96,7 @@ class MoveUploadsToNewDisk extends Command
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
$private_uploads['assetmodels'] = glob('storage/private_uploads/models'."/*.*");
$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'."/*.*");

View File

@@ -59,7 +59,7 @@ class ObjectImportCommand extends Command
$classString = "App\\Importer\\{$class}Importer";
$importer = new $classString($filename);
$importer->setCallbacks([$this, 'log'], [$this, 'progress'], [$this, 'errorCallback'])
->setCreatedBy($this->option('user_id'))
->setUserId($this->option('user_id'))
->setUpdating($this->option('update'))
->setShouldNotify($this->option('send-welcome'))
->setUsernameFormat($this->option('username_format'));

View File

@@ -4,7 +4,7 @@ namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use Illuminate\Support\Facades\Schema;
use Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
@@ -51,7 +51,8 @@ class PaveIt extends Command
}
// List all the tables in the database so we don't have to worry about missing some as the app grows
$tables = Schema::getTables();
$tables = DB::connection()->getDoctrineSchemaManager()->listTableNames();
$except_tables = [
'oauth_access_tokens',
'oauth_clients',
@@ -66,15 +67,14 @@ class PaveIt extends Command
foreach ($custom_fields as $custom_field) {
$this->info('DROP the '.$custom_field->db_column.' column from assets as well.');
if (Schema::hasColumn('assets', $custom_field->db_column)) {
Schema::table('assets', function ($table) use ($custom_field) {
if (\Schema::hasColumn('assets', $custom_field->db_column)) {
\Schema::table('assets', function ($table) use ($custom_field) {
$table->dropColumn($custom_field->db_column);
});
}
}
foreach ($tables as $table_obj) {
$table = $table_obj['name'];
foreach ($tables as $table) {
if (in_array($table, $except_tables)) {
$this->info($table. ' is SKIPPED.');
} else {
@@ -84,8 +84,8 @@ class PaveIt extends Command
}
// Leave in the demo oauth keys so we don't have to reset them every day in the demos
DB::statement('delete from oauth_clients WHERE id > 2');
DB::statement('delete from oauth_access_tokens WHERE user_id > 2');
\DB::statement('delete from oauth_clients WHERE id > 2');
\DB::statement('delete from oauth_access_tokens WHERE id > 2');
}
}

View File

@@ -62,19 +62,19 @@ class Purge extends Command
$assetcount = $assets->count();
$this->info($assets->count().' assets purged.');
$asset_assoc = 0;
$maintenances = 0;
$asset_maintenances = 0;
foreach ($assets as $asset) {
$this->info('- Asset "'.$asset->present()->name().'" deleted.');
$asset_assoc += $asset->assetlog()->count();
$asset->assetlog()->forceDelete();
$maintenances += $asset->maintenances()->count();
$asset->maintenances()->forceDelete();
$asset_maintenances += $asset->assetmaintenances()->count();
$asset->assetmaintenances()->forceDelete();
$asset->forceDelete();
}
$this->info($asset_assoc.' corresponding log records purged.');
$this->info($maintenances.' corresponding maintenance records purged.');
$this->info($asset_maintenances.' corresponding maintenance records purged.');
$locations = Location::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($locations->count().' locations purged.');

View File

@@ -0,0 +1,157 @@
<?php
namespace App\Console\Commands;
use App\LegacyEncrypter\McryptEncrypter;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Setting;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class RecryptFromMcrypt extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:legacy-recrypt
{--force : Force a re-crypt of encrypted data from MCRYPT.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command allows upgrading users to de-encrypt their deprecated mcrypt encrypted fields and re-encrypt them using the current OpenSSL encryption.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Check and see if they have a legacy app key listed in their .env
// If not, we can try to use the current APP_KEY if looks like it's old
$legacy_key = env('LEGACY_APP_KEY');
$key_parts = explode(':', $legacy_key);
$legacy_cipher = env('LEGACY_CIPHER', 'rijndael-256');
$errors = [];
if (! $legacy_key) {
$this->error('ERROR: You do not have a LEGACY_APP_KEY set in your .env file. Please locate your old APP_KEY and ADD a line to your .env file like: LEGACY_APP_KEY=YOUR_OLD_APP_KEY');
return false;
}
// Do some basic legacy app key length checks
if (strlen($legacy_key) == 32) {
$legacy_length_check = true;
} elseif (array_key_exists('1', $key_parts) && (strlen($key_parts[1]) == 44)) {
$legacy_key = base64_decode($key_parts[1], true);
$legacy_length_check = true;
} else {
$legacy_length_check = false;
}
// Check that the app key is 32 characters
if ($legacy_length_check === true) {
$this->comment('INFO: Your LEGACY_APP_KEY looks correct. Okay to continue.');
} else {
$this->error('ERROR: Your LEGACY_APP_KEY is not the correct length (32 characters or base64 followed by 44 characters for later versions). Please locate your old APP_KEY and use that as your LEGACY_APP_KEY in your .env file to continue.');
return false;
}
$this->error('================================!!!! WARNING !!!!================================');
$this->error('================================!!!! WARNING !!!!================================');
$this->comment("This tool will attempt to decrypt your old Snipe-IT (mcrypt, now deprecated) encrypted data and re-encrypt it using OpenSSL. \n\nYou should only continue if you have backed up any and all old APP_KEYs and have backed up your data.");
$force = ($this->option('force')) ? true : false;
if ($force || ($this->confirm('Are you SURE you wish to continue?'))) {
$backup_file = 'backups/env-backups/'.'app_key-'.date('Y-m-d-gis');
try {
Storage::disk('local')->put($backup_file, 'APP_KEY: '.config('app.key'));
Storage::disk('local')->append($backup_file, 'LEGACY_APP_KEY: '.$legacy_key);
} catch (\Exception $e) {
$this->info('WARNING: Could not backup app keys');
}
if ($legacy_cipher) {
$mcrypter = new McryptEncrypter($legacy_key, $legacy_cipher);
} else {
$mcrypter = new McryptEncrypter($legacy_key);
}
$settings = Setting::getSettings();
if ($settings->ldap_pword == '') {
$this->comment('INFO: No LDAP password found. Skipping... ');
} else {
$decrypted_ldap_pword = $mcrypter->decrypt($settings->ldap_pword);
$settings->ldap_pword = Crypt::encrypt($decrypted_ldap_pword);
$settings->save();
}
/** @var CustomField[] $custom_fields */
$custom_fields = CustomField::where('field_encrypted', '=', 1)->get();
$this->comment('INFO: Retrieving encrypted custom fields...');
$query = Asset::withTrashed();
foreach ($custom_fields as $custom_field) {
$this->comment('FIELD TO RECRYPT: '.$custom_field->name.' ('.$custom_field->db_column.')');
$query->orWhereNotNull($custom_field->db_column);
}
// Get all assets with a value in any of the fields that were encrypted
/** @var Asset[] $assets */
$assets = $query->get();
$bar = $this->output->createProgressBar(count($assets));
foreach ($assets as $asset) {
foreach ($custom_fields as $encrypted_field) {
$columnName = $encrypted_field->db_column;
// Make sure the value isn't null
if ($asset->{$columnName} != '') {
// Try to decrypt the payload using the legacy app key
try {
$decrypted_field = $mcrypter->decrypt($asset->{$columnName});
$asset->{$columnName} = Crypt::encrypt($decrypted_field);
$this->comment($decrypted_field);
} catch (\Exception $e) {
$errors[] = ' - ERROR: Could not decrypt field ['.$encrypted_field->name.']: '.$e->getMessage();
}
}
}
$asset->save();
$bar->advance();
}
$bar->finish();
if (count($errors) > 0) {
$this->comment("\n\n");
$this->error("The decrypter encountered some errors: \n");
foreach ($errors as $error) {
$this->error($error);
}
}
}
}
}

View File

@@ -50,12 +50,12 @@ class ResetDemoSettings extends Command
$settings->alert_email = 'service@snipe-it.io';
$settings->login_note = 'Use `admin` / `password` to login to the demo.';
$settings->header_color = null;
$settings->label2_2d_type = 'QRCODE';
$settings->barcode_type = 'QRCODE';
$settings->default_currency = 'USD';
$settings->brand = 2;
$settings->ldap_enabled = 0;
$settings->full_multiple_companies_support = 0;
$settings->label2_1d_type = 'C128';
$settings->alt_barcode = 'C128';
$settings->skin = '';
$settings->email_domain = 'snipeitapp.com';
$settings->email_format = 'filastname';
@@ -65,7 +65,7 @@ class ResetDemoSettings extends Command
$settings->thumbnail_max_h = '30';
$settings->locale = 'en-US';
$settings->version_footer = 'on';
$settings->support_footer = 'on';
$settings->support_footer = null;
$settings->saml_enabled = '0';
$settings->saml_sp_x509cert = null;
$settings->saml_idp_metadata = null;

View File

@@ -51,8 +51,6 @@ class SQLStreamer {
/* we *could* have made the ^INSERT INTO blah VALUES$ turn on the capturing state, and closed it with
a ^(blahblah);$ but it's cleaner to not have to manage the state machine. We're just going to
assume that (blahblah), or (blahblah); are values for INSERT and are always acceptable. */
"<^/\*!40101 SET NAMES '?[a-zA-Z0-9_-]+'? \*/;$>" => false, //using weird delimiters (<,>) for readability. allow quoted or unquoted charsets
"<^/\*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' \*/;$>" => false, //same, now handle zero-values
];
foreach($allowed_statements as $statement => $statechange) {
@@ -243,15 +241,12 @@ class RestoreFromBackup extends Command
$private_dirs = [
'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels',
'storage/private_uploads/maintenances',
'storage/private_uploads/models',
'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/locations',
'storage/private_uploads/licenses',
'storage/private_uploads/signatures',
'storage/private_uploads/users',
@@ -262,10 +257,9 @@ class RestoreFromBackup extends Command
];
$public_dirs = [
'public/uploads/accessories',
'public/uploads/assetmodels',
'public/uploads/maintenances',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/avatars',
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/categories',
'public/uploads/companies',
'public/uploads/components',
@@ -293,7 +287,6 @@ class RestoreFromBackup extends Command
$interesting_files = [];
$boring_files = [];
$unsafe_files = [];
for ($i = 0; $i < $za->numFiles; $i++) {
$stat_results = $za->statIndex($i);
@@ -332,9 +325,8 @@ class RestoreFromBackup extends Command
}
}
}
$good_extensions = config('filesystems.allowed_upload_extensions_array');
$good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
'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) {
@@ -344,9 +336,7 @@ class RestoreFromBackup extends Command
if ($last_pos !== false) {
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
if (!in_array($extension, $good_extensions)) {
// gathering potentially unsafe files here to return at exit
$unsafe_files[] = $raw_path;
Log::debug('Potentially unsafe file '.$raw_path.' is being skipped');
$this->warn('Potentially unsafe file ' . $raw_path . ' is being skipped');
$boring_files[] = $raw_path;
continue 2;
}
@@ -380,8 +370,7 @@ class RestoreFromBackup extends Command
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 sanitize your SQL.");
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitze your SQL.");
}
// If we're doing --sql-stdout-only, handle that now so we don't have to open pipes to mysql and all of that silliness
@@ -514,11 +503,6 @@ class RestoreFromBackup extends Command
} else {
$this->info(count($interesting_files).' files were succesfully transferred');
}
if (count($unsafe_files) > 0) {
foreach ($unsafe_files as $unsafe_file) {
$this->warn('Potentially unsafe file '.$unsafe_file.' was skipped');
}
}
foreach ($boring_files as $boring_file) {
$this->warn($boring_file.' was skipped.');
}

View File

@@ -64,46 +64,28 @@ class SendAcceptanceReminder extends Command
->groupBy(function($item) {
return $item['acceptance']->assignedTo ? $item['acceptance']->assignedTo->id : '';
});
$no_email_list= [];
foreach($unacceptedAssetGroups as $unacceptedAssetGroup) {
// The [0] is weird, but it allows for the item_count to work and grabs the appropriate info for each user.
// Collapsing and flattening the collection doesn't work above.
$acceptance = $unacceptedAssetGroup[0]['acceptance'];
$locale = $acceptance->assignedTo?->locale;
$email = $acceptance->assignedTo?->email;
if(!$email){
$no_email_list[] = [
'id' => $acceptance->assignedTo?->id,
'name' => $acceptance->assignedTo?->present()->fullName(),
];
} else {
$count++;
$this->info($acceptance->assignedTo->present()->fullName().' has no email address.');
}
$item_count = $unacceptedAssetGroup->count();
if ($locale && $email) {
Mail::to($email)->send((new UnacceptedAssetReminderMail($acceptance, $item_count))->locale($locale));
} elseif ($email) {
Mail::to($email)->send((new UnacceptedAssetReminderMail($acceptance, $item_count)));
}
$count++;
}
$this->info($count.' users notified.');
$headers = ['ID', 'Name'];
$rows = [];
foreach ($no_email_list as $user) {
$rows[] = [$user['id'], $user['name']];
}
if (!empty($rows)) {
$this->info("The following users do not have an email address:");
$this->table($headers, $rows);
}
return 0;
}

View File

@@ -2,13 +2,13 @@
namespace App\Console\Commands;
use App\Mail\ExpiringAssetsMail;
use App\Mail\ExpiringLicenseMail;
use App\Models\Asset;
use App\Models\License;
use App\Models\Recipients\AlertRecipient;
use App\Models\Setting;
use App\Notifications\ExpiringAssetsNotification;
use App\Notifications\ExpiringLicenseNotification;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendExpirationAlerts extends Command
{
@@ -42,28 +42,27 @@ class SendExpirationAlerts extends Command
public function handle()
{
$settings = Setting::getSettings();
$alert_interval = $settings->alert_interval;
$threshold = $settings->alert_interval;
if (($settings->alert_email != '') && ($settings->alerts_enabled == 1)) {
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))
->map(fn($item) => trim($item)) // Trim each email
->filter(fn($item) => !empty($item))
->all();
// Expiring Assets
$assets = Asset::getExpiringWarrantee($alert_interval);
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item, $key) {
return new AlertRecipient($item);
});
// Expiring Assets
$assets = Asset::getExpiringWarrantee($threshold);
if ($assets->count() > 0) {
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval));
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $threshold]));
\Notification::send($recipients, new ExpiringAssetsNotification($assets, $threshold));
}
// Expiring licenses
$licenses = License::getExpiringLicenses($alert_interval);
$licenses = License::getExpiringLicenses($threshold);
if ($licenses->count() > 0) {
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
Mail::to($recipients)->send(new ExpiringLicenseMail($licenses, $alert_interval));
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $threshold]));
\Notification::send($recipients, new ExpiringLicenseNotification($licenses, $threshold));
}
} else {
if ($settings->alert_email == '') {

View File

@@ -2,12 +2,13 @@
namespace App\Console\Commands;
use App\Mail\SendUpcomingAuditMail;
use App\Models\Asset;
use App\Models\Recipients\AlertRecipient;
use App\Models\Setting;
use App\Notifications\SendUpcomingAuditNotification;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendUpcomingAuditReport extends Command
{
@@ -47,20 +48,19 @@ class SendUpcomingAuditReport extends Command
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
$assets = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get();
$this->info($assets->count() . ' assets must be audited in on or before ' . $interval_date . ' is deadline');
$assets = Asset::whereNull('deleted_at')->DueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get();
$this->info($assets->count().' assets must be audited in on or before '.$interval_date.' is deadline');
if ((count($assets) !== 0) && ($assets->count() > 0) && ($settings->alert_email != '')) {
if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))
->map(fn($item) => trim($item))
->filter(fn($item) => !empty($item))
->all();
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item) {
return new AlertRecipient($item);
});
$this->info('Sending Admin SendUpcomingAuditNotification to: '.$settings->alert_email);
\Notification::send($recipients, new SendUpcomingAuditNotification($assets, $settings->audit_warning_days));
$this->info('Sending Admin SendUpcomingAuditNotification to: ' . $settings->alert_email);
Mail::to($recipients)->send(new SendUpcomingAuditMail($assets, $settings->audit_warning_days));
}
}

View File

@@ -1,51 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Helpers\Helper;
use Illuminate\Console\Command;
class TestLocationsFMCS extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:test-locations-fmcs {--location_id=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Test for company ID inconsistencies if FullMultipleCompanySupport with scoped locations will be used.';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('This script checks for company ID inconsistencies if Full Multiple Company Support with scoped locations will be used.');
$this->info('This could take a few moments if have a very large dataset.');
$this->newLine();
// if parameter location_id is set, only test this location
$location_id = null;
if ($this->option('location_id')) {
$location_id = $this->option('location_id');
}
$mismatched = Helper::test_locations_fmcs(true, $location_id);
$this->warn(trans_choice('admin/settings/message.location_scoping.mismatch', count($mismatched)));
$this->newLine();
$this->info('Edit your locations to associate them with the correct company.');
$header = ['Type', 'ID', 'Name', 'Checkout Type', 'Company ID', 'Item Company', 'Item Location', 'Location Company', 'Location Company ID'];
sort($mismatched);
$this->table($header, $mismatched);
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Console;
use App\Console\Commands\ImportLocations;
use App\Console\Commands\ReEncodeCustomFieldNames;
use App\Console\Commands\RestoreDeletedUsers;
use App\Models\Setting;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -19,14 +18,12 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
if(Setting::getSettings()?->alerts_enabled === 1) {
$schedule->command('snipeit:inventory-alerts')->daily();
$schedule->command('snipeit:expiring-alerts')->daily();
$schedule->command('snipeit:expected-checkin')->daily();
$schedule->command('snipeit:upcoming-audits')->daily();
}
$schedule->command('snipeit:inventory-alerts')->daily();
$schedule->command('snipeit:expiring-alerts')->daily();
$schedule->command('snipeit:expected-checkin')->daily();
$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();
}

View File

@@ -28,7 +28,7 @@ class CheckoutableCheckedIn
$this->checkedOutTo = $checkedOutTo;
$this->checkedInBy = $checkedInBy;
$this->note = $note;
$this->action_date = $action_date ?? date('Y-m-d H:i:s');
$this->action_date = $action_date ?? date('Y-m-d');
$this->originalValues = $originalValues;
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class AssetNotRequestable extends Exception
{
}

View File

@@ -11,7 +11,6 @@ use Illuminate\Support\Facades\Log;
use Throwable;
use JsonException;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
class Handler extends ExceptionHandler
{
@@ -108,66 +107,21 @@ class Handler extends ExceptionHandler
$statusCode = $e->getStatusCode();
// API throttle requests are handled in the RouteServiceProvider configureRateLimiting() method, so we don't need to handle them here
switch ($e->getStatusCode()) {
case '404':
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode . ' endpoint not found'), 404);
case '429':
return response()->json(Helper::formatStandardApiResponse('error', null, 'Too many requests'), 429);
case '405':
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
default:
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode);
}
}
// This handles API validation exceptions that happen at the Form Request level, so they
// never even get to the controller where we normally nicely format JSON responses
if ($e instanceof ValidationException) {
$response = $this->invalidJson($request, $e);
return response()->json(Helper::formatStandardApiResponse('error', null, $e->errors()), 200);
}
}
// This is traaaaash but it handles models that are not found while using route model binding :(
// The only alternative is to set that at *each* route, which is crazypants
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
$ids = method_exists($e, 'getIds') ? $e->getIds() : [];
if (in_array('bulkedit', $ids, true)) {
$error_array = session()->get('bulk_asset_errors');
return redirect()
->route('hardware.index')
->withErrors($error_array, 'bulk_asset_errors')
->withInput();
}
// This gets the MVC model name from the exception and formats in a way that's less fugly
$model_name = trim(strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel()))))));
$route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
// Sigh.
if ($route == 'assets.index') {
$route = 'hardware.index';
} elseif ($route == 'reporttemplates.index') {
$route = 'reports/custom';
} elseif ($route == 'assetmodels.index') {
$route = 'models.index';
} elseif ($route == 'predefinedkits.index') {
$route = 'kits.index';
} elseif ($route == 'assetmaintenances.index') {
$route = 'maintenances.index';
} elseif ($route === 'licenseseats.index') {
$route = 'licenses.index';
} elseif (($route === 'customfieldsets.index') || ($route === 'customfields.index')) {
$route = 'fields.index';
}
return redirect()
->route($route)
->withError(trans('general.generic_model_not_found', ['model' => $model_name]));
}
if ($this->isHttpException($e) && (isset($statusCode)) && ($statusCode == '404' )) {
@@ -220,9 +174,8 @@ class Handler extends ExceptionHandler
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
});
}
}
}

View File

@@ -1,10 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class UserDoestExistException extends Exception
{
}

View File

@@ -12,8 +12,6 @@ use App\Models\Depreciation;
use App\Models\Setting;
use App\Models\Statuslabel;
use App\Models\License;
use App\Models\Location;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Contracts\Encryption\DecryptException;
use Carbon\Carbon;
@@ -723,8 +721,8 @@ class Helper
// The check and message that the user is still using the deprecated version
$deprecations = [
'ms_teams_deprecated' => array(
'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows') && (Setting::getSettings()->webhook_selected === 'microsoft'),
'message' => 'The Microsoft Teams webhook URL being used will be deprecated Dec 31st, 2025. <a class="btn btn-primary" href="' . route('settings.slack.index') . '">Change webhook endpoint</a>'),
'check' => !Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows'),
'message' => 'The Microsoft Teams webhook URL being used will be deprecated Jan 31st, 2025. <a class="btn btn-primary" href="' . route('settings.slack.index') . '">Change webhook endpoint</a>'),
];
// if item of concern is being used and its being used with the deprecated values return the notification array.
@@ -870,49 +868,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') || ($filetype == 'image/webp')) {
return $filetype;
}
return false;
}
/**
* Check if the file is a video, so we can show a preview
*
* @param File $file
* @return string | Boolean
* @author [B. Wetherington] [<bwetherington@grokability.com>]
* @since [v8.1.18]
*/
public static function checkUploadIsVideo($file)
{
$finfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
$filetype = @finfo_file($finfo, $file);
finfo_close($finfo);
if (($filetype == 'video/mp4') || ($filetype == 'video/quicktime') || ($filetype == 'video/mpeg') || ($filetype == 'video/ogg') || ($filetype == 'video/webm') || ($filetype == 'video/x-msvide')) {
return $filetype;
}
return false;
}
/**
* Check if the file is audio, so we can show a preview
*
* @param File $file
* @return string | Boolean
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
*/
public static function checkUploadIsAudio($file)
{
$finfo = @finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
$filetype = @finfo_file($finfo, $file);
finfo_close($finfo);
if (($filetype == 'audio/mpeg') || ($filetype == 'audio/ogg')) {
if (($filetype == 'image/jpeg') || ($filetype == 'image/jpg') || ($filetype == 'image/png') || ($filetype == 'image/bmp') || ($filetype == 'image/gif') || ($filetype == 'image/avif')) {
return $filetype;
}
@@ -939,12 +895,6 @@ class Helper
public static function selectedPermissionsArray($permissions, $selected_arr = [])
{
$permissions_arr = [];
if (is_array($permissions)) {
$permissions = json_encode($permissions);
}
// Set default to empty JSON if the value is null
$permissions = json_decode($permissions ?? '{}', JSON_OBJECT_AS_ARRAY);
foreach ($permissions as $permission) {
for ($x = 0; $x < count($permission); $x++) {
@@ -955,13 +905,13 @@ class Helper
if (is_array($selected_arr)) {
if (array_key_exists($permission_name, $selected_arr)) {
$permissions_arr[$permission_name] = (int) $selected_arr[$permission_name];
$permissions_arr[$permission_name] = $selected_arr[$permission_name];
} else {
$permissions_arr[$permission_name] = 0;
$permissions_arr[$permission_name] = '0';
}
} else {
$permissions_arr[$permission_name] = 0;
$permissions_arr[$permission_name] = '0';
}
}
}
@@ -1197,42 +1147,22 @@ class Helper
'webp' => 'far fa-image',
'avif' => 'far fa-image',
'svg' => 'fas fa-vector-square',
// word
'doc' => 'far fa-file-word',
'docx' => 'far fa-file-word',
// Excel
'xls' => 'far fa-file-excel',
'xlsx' => 'far fa-file-excel',
'ods' => 'far fa-file-excel',
// Presentation
'ppt' => 'far fa-file-powerpoint',
'odp' => 'far fa-file-powerpoint',
// archive
'zip' => 'fas fa-file-archive',
'rar' => 'fas fa-file-archive',
//Text
'odt' => 'far fa-file-alt',
'txt' => 'far fa-file-alt',
'rtf' => 'far fa-file-alt',
'xml' => 'fas fa-code',
// Misc
'pdf' => 'far fa-file-pdf',
'lic' => 'far fa-save',
// video
'mov' => 'fa-solid fa-video',
'mp4' => 'fa-solid fa-video',
// audio
'ogg' => 'fa-solid fa-file-audio',
'mp3' => 'fa-solid fa-file-audio',
'wav' => 'fa-solid fa-file-audio',
];
if ($extension && array_key_exists($extension, $allowedExtensionMap)) {
@@ -1376,24 +1306,25 @@ class Helper
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';
case 'supplier':
return 'fa-solid fa-store';
case 'manufacturer':
return 'fa-solid fa-building';
case 'category':
return 'fa-solid fa-table-columns';
break;
}
}
@@ -1543,168 +1474,59 @@ class Helper
}
static public function getRedirectOption($request, $id, $table, $item_id = null) : RedirectResponse
static public function getRedirectOption($request, $id, $table, $item_id = null)
{
$redirect_option = Session::get('redirect_option') ?? $request->redirect_option;
$checkout_to_type = Session::get('checkout_to_type') ?? null;
$checkedInFrom = Session::get('checkedInFrom');
$other_redirect = Session::get('other_redirect');
$backUrl = Session::pull('back_url', route('home'));
// return to previous page
if ($redirect_option === 'back') {
return redirect()->to($backUrl);
}
$redirect_option = Session::get('redirect_option');
$checkout_to_type = Session::get('checkout_to_type');
// return to index
if ($redirect_option == 'index') {
return match ($table) {
'Assets' => redirect()->route('hardware.index'),
'Users' => redirect()->route('users.index'),
'Licenses' => redirect()->route('licenses.index'),
'Accessories' => redirect()->route('accessories.index'),
'Components' => redirect()->route('components.index'),
'Consumables' => redirect()->route('consumables.index'),
};
switch ($table) {
case "Assets":
return route('hardware.index');
case "Users":
return route('users.index');
case "Licenses":
return route('licenses.index');
case "Accessories":
return route('accessories.index');
case "Components":
return route('components.index');
case "Consumables":
return route('consumables.index');
}
}
// return to thing being assigned
if ($redirect_option == 'item') {
return match ($table) {
'Assets' => redirect()->route('hardware.show', $id ?? $item_id),
'Users' => redirect()->route('users.show', $id ?? $item_id),
'Licenses' => redirect()->route('licenses.show', $id ?? $item_id),
'Accessories' => redirect()->route('accessories.show', $id ?? $item_id),
'Components' => redirect()->route('components.show', $id ?? $item_id),
'Consumables' => redirect()->route('consumables.show', $id ?? $item_id),
};
switch ($table) {
case "Assets":
return route('hardware.show', $id ?? $item_id);
case "Users":
return route('users.show', $id ?? $item_id);
case "Licenses":
return route('licenses.show', $id ?? $item_id);
case "Accessories":
return route('accessories.show', $id ?? $item_id);
case "Components":
return route('components.show', $id ?? $item_id);
case "Consumables":
return route('consumables.show', $id ?? $item_id);
}
}
// return to assignment target
if ($redirect_option == 'target') {
return match ($checkout_to_type) {
'user' => redirect()->route('users.show', $request->assigned_user ?? $checkedInFrom),
'location' => redirect()->route('locations.show', $request->assigned_location ?? $checkedInFrom),
'asset' => redirect()->route('hardware.show', $request->assigned_asset ?? $checkedInFrom),
};
switch ($checkout_to_type) {
case 'user':
return route('users.show', ['user' => $request->assigned_user]);
case 'location':
return route('locations.show', ['location' => $request->assigned_location]);
case 'asset':
return route('hardware.show', ['hardware' => $request->assigned_asset]);
}
}
// return to somewhere else
if ($redirect_option == 'other_redirect') {
return match ($other_redirect) {
'audit' => redirect()->route('assets.audit.due'),
'model' => redirect()->route('models.show', $request->model_id),
};
}
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'));
}
/**
* Check for inconsistencies before activating scoped locations with FullMultipleCompanySupport
* If there are locations with different companies than related objects unforseen problems could arise
*
* @author T. Regnery <tobias.regnery@gmail.com>
* @since 7.0
*
* @param $artisan when false, bail out on first inconsistent entry
* @param $location_id when set, only test this specific location
* @param $new_company_id in case of updating a location, this is the newly requested company_id
* @return string []
*/
static public function test_locations_fmcs($artisan, $location_id = null, $new_company_id = null) {
$mismatched = [];
if ($location_id) {
$location = Location::find($location_id);
if ($location) {
$locations = collect([])->push(Location::find($location_id));
}
} else {
$locations = Location::all();
}
// Bail out early if there are no locations
if ($locations->count() == 0) {
return [];
}
foreach($locations as $location) {
// in case of an update of a single location, use the newly requested company_id
if ($new_company_id) {
$location_company = $new_company_id;
} else {
$location_company = $location->company_id;
}
// Depending on the relationship, we must use different operations to retrieve the objects
$keywords_relation = [
'many' => [
'accessories',
'assets',
'assignedAccessories',
'assignedAssets',
'components',
'consumables',
'rtd_assets',
'users',
],
'one' => [
'manager',
'parent',
]];
// In case of a single location, the children must be checked as well, because we don't walk every location
if ($location_id) {
$keywords_relation['many'][] = 'children';
}
foreach ($keywords_relation as $relation => $keywords) {
foreach($keywords as $keyword) {
if ($relation == 'many') {
$items = $location->{$keyword}->all();
} else {
$items = collect([])->push($location->$keyword);
}
$count = 0;
foreach ($items as $item) {
if ($item && $item->company_id != $location_company) {
$mismatched[] = [
class_basename(get_class($item)),
$item->id,
$item->name ?? $item->asset_tag ?? $item->serial ?? $item->username,
$item->assigned_type ? str_replace('App\\Models\\', '', $item->assigned_type) : null,
$item->company_id ?? null,
$item->company->name ?? null,
// $item->defaultLoc->id ?? null,
// $item->defaultLoc->name ?? null,
// $item->defaultLoc->company->id ?? null,
// $item->defaultLoc->company->name ?? null,
$item->location->name ?? null,
$item->location->company->name ?? null,
$location_company ?? null,
];
$count++;
// Bail early if this is not being run via artisan
if ((!$artisan) && ($count > 0)) {
return $mismatched;
}
}
}
}
}
}
return $mismatched;
}
}

View File

@@ -43,8 +43,6 @@ class IconHelper
return 'fa-regular fa-envelope';
case 'phone':
return 'fa-solid fa-phone';
case 'mobile':
return 'fas fa-mobile-screen-button';
case 'long-arrow-right':
return 'fas fa-long-arrow-alt-right';
case 'download':
@@ -61,8 +59,6 @@ class IconHelper
return 'fas fa-cog';
case 'angle-left':
return 'fas fa-angle-left';
case 'angle-right':
return 'fas fa-angle-right';
case 'warning':
return 'fas fa-exclamation-triangle';
case 'kits':
@@ -153,7 +149,6 @@ class IconHelper
case 'location':
return 'fas fa-map-marker-alt';
case 'superadmin':
case 'admin':
return 'fas fa-crown';
case 'print':
return 'fa-solid fa-print';
@@ -189,11 +184,7 @@ class IconHelper
return 'fa-regular fa-id-card';
case 'department' :
return 'fa-solid fa-building-user';
case 'home' :
return 'fa-solid fa-house';
case 'note':
case 'notes':
return 'fas fa-sticky-note';
}
}
}

View File

@@ -16,84 +16,38 @@ class StorageHelper
$disk = config('filesystems.default');
}
switch (config("filesystems.disks.$disk.driver")) {
case 'local':
return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
case 'local':
return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
case 's3':
return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
case 's3':
return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
default:
return Storage::disk($disk)->download($filename);
default:
return Storage::disk($disk)->download($filename);
}
}
public static function getMediaType($file_with_path) {
// Get the file extension and determine the media type
if (Storage::exists($file_with_path)) {
$fileinfo = pathinfo($file_with_path);
$extension = strtolower($fileinfo['extension']);
switch ($extension) {
case 'avif':
case 'jpg':
case 'png':
case 'gif':
case 'svg':
case 'webp':
return 'image';
case 'pdf':
return 'pdf';
case 'mp3':
case 'wav':
case 'ogg':
return 'audio';
case 'mp4':
case 'webm':
case 'mov':
return 'video';
case 'doc':
case 'docx':
return 'document';
case 'txt':
return 'text';
case 'xls':
case 'xlsx':
case 'ods':
return 'spreadsheet';
default:
return $extension; // Default for unknown types
}
}
return null;
}
/**
* This determines the file types that should be allowed inline and checks their fileinfo extension
* to determine that they are safe to display inline.
*
* @author <A. Gianotto> [<snipe@snipe.net]>
* @since v7.0.14
* @param $file_with_path
* @since v7.0.14
* @param $file_with_path
* @return bool
*/
public static function allowSafeInline($file_with_path)
{
public static function allowSafeInline($file_with_path) {
$allowed_inline = [
'avif',
'gif',
'gif',
'jpg',
'mov',
'mp3',
'mp4',
'ogg',
'pdf',
'png',
'svg',
'wav',
'webm',
'jpg',
'gif',
'svg',
'avif',
'webp',
'png',
];
@@ -105,24 +59,10 @@ class StorageHelper
}
public static function getFiletype($file_with_path)
{
// The file exists and is allowed to be displayed inline
if (Storage::exists($file_with_path)) {
return pathinfo($file_with_path, PATHINFO_EXTENSION);
}
return null;
}
/**
* Decide whether to show the file inline or download it.
*/
public static function showOrDownloadFile($file, $filename)
{
public static function showOrDownloadFile($file, $filename) {
$headers = [];

View File

@@ -31,7 +31,7 @@ class AccessoriesController extends Controller
public function index() : View
{
$this->authorize('index', Accessory::class);
return view('accessories.index');
return view('accessories/index');
}
/**
@@ -77,30 +77,13 @@ class AccessoriesController extends Controller
$accessory->supplier_id = request('supplier_id');
$accessory->notes = request('notes');
if ($request->has('use_cloned_image')) {
$cloned_model_img = Accessory::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'accessories/'.$new_image_name;
Storage::disk('public')->copy('accessories/'.$cloned_model_img->image, $new_image);
$accessory->image = $new_image_name;
}
} else {
$accessory = $request->handleImages($accessory);
}
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
$accessory = $request->handleImages($accessory);
session()->put(['redirect_option' => $request->get('redirect_option')]);
// Was the accessory created?
if ($accessory->save()) {
// Redirect to the new accessory page
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.create.success'));
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($accessory->getErrors());
@@ -112,10 +95,16 @@ class AccessoriesController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $accessoryId
*/
public function edit(Accessory $accessory) : View | RedirectResponse
public function edit($accessoryId = null) : View | RedirectResponse
{
$this->authorize('update', Accessory::class);
return view('accessories.edit')->with('item', $accessory)->with('category_type', 'accessory');
if ($item = Accessory::find($accessoryId)) {
$this->authorize($item);
return view('accessories/edit', compact('item'))->with('category_type', 'accessory');
}
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
}
/**
@@ -125,18 +114,24 @@ class AccessoriesController extends Controller
* @param int $accessoryId
* @since [v6.0]
*/
public function getClone(Accessory $accessory) : View | RedirectResponse
public function getClone($accessoryId = null) : View | RedirectResponse
{
$this->authorize('create', Accessory::class);
$cloned = clone $accessory;
$accessory_to_clone = $accessory;
$cloned->id = null;
$cloned->deleted_at = '';
// 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('cloned_model', $accessory_to_clone)
->with('item', $cloned);
->with('item', $accessory);
}
@@ -147,9 +142,9 @@ class AccessoriesController extends Controller
* @param ImageUploadRequest $request
* @param int $accessoryId
*/
public function update(ImageUploadRequest $request, Accessory $accessory) : RedirectResponse
public function update(ImageUploadRequest $request, $accessoryId = null) : RedirectResponse
{
if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessory->id)) {
if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryId)) {
$this->authorize($accessory);
@@ -185,8 +180,7 @@ class AccessoriesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($accessory->save()) {
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.update.success'));
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.update.success'));
}
} else {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
@@ -203,15 +197,15 @@ class AccessoriesController extends Controller
*/
public function destroy($accessoryId) : RedirectResponse
{
if (is_null($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryId))) {
if (is_null($accessory = Accessory::find($accessoryId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
}
$this->authorize($accessory);
if ($accessory->checkouts_count > 0) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/general.delete_disabled'));
if ($accessory->hasUsers() > 0) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.assoc_users', ['count'=> $accessory->hasUsers()]));
}
if ($accessory->image) {
@@ -237,13 +231,14 @@ class AccessoriesController extends Controller
* @see AccessoriesController::getDataView() method that generates the JSON response
* @since [v1.0]
*/
public function show(Accessory $accessory) : View | RedirectResponse
public function show($accessoryID = null) : View | RedirectResponse
{
$accessory->loadCount('checkouts as checkouts_count');
$accessory->load(['adminuser' => fn($query) => $query->withTrashed()]);
$accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryID);
$this->authorize('view', $accessory);
return view('accessories.view', compact('accessory'));
if (isset($accessory->id)) {
return view('accessories/view', compact('accessory'));
}
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist', ['id' => $accessoryID]));
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\Http\Controllers\Accessories;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Accessory;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class AccessoriesFilesController extends Controller
{
/**
* Validates and stores files associated with a accessory.
*
* @param UploadFileRequest $request
* @param int $accessoryId
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(UploadFileRequest $request, $accessoryId = null) : RedirectResponse
{
if (config('app.lock_passwords')) {
return redirect()->route('accessories.show', ['accessory'=>$accessoryId])->with('error', trans('general.feature_disabled'));
}
$accessory = Accessory::find($accessoryId);
if (isset($accessory->id)) {
$this->authorize('accessories.files', $accessory);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/accessories')) {
Storage::makeDirectory('private_uploads/accessories', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/accessories/', 'accessory-'.$accessory->id, $file);
//Log the upload to the log
$accessory->logUpload($file_name, e($request->input('notes')));
}
return redirect()->route('accessories.show', $accessory->id)->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
*/
public function destroy($accessoryId = null, $fileId = null) : RedirectResponse
{
$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
*/
public function show($accessoryId = null, $fileId = null) : View | RedirectResponse | Response | BinaryFileResponse | StreamedResponse
{
// the accessory is valid
if ($accessory = Accessory::find($accessoryId)) {
$this->authorize('view', $accessory);
$this->authorize('accessories.files', $accessory);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $accessory->id)->find($fileId)) {
$file = 'private_uploads/accessories/'.$log->filename;
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('accessories.show', ['accessory' => $accessory])->with('error', trans('general.file_not_found'));
}
}
return redirect()->route('accessories.show', ['accessory' => $accessory])->with('error', trans('general.log_record_not_found'));
}
return redirect()->route('accessories.index')->with('error', trans('general.file_not_found'));
}
}

View File

@@ -7,6 +7,7 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use \Illuminate\Contracts\View\View;
@@ -29,17 +30,9 @@ class AccessoryCheckinController extends Controller
}
$accessory = Accessory::find($accessory_user->accessory_id);
//based on what the accessory is checked out to the target redirect option will be displayed accordingly.
$target_option = match ($accessory_user->assigned_type) {
'App\Models\Asset' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.asset')]),
'App\Models\Location' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.location')]),
default => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.user')]),
};
$this->authorize('checkin', $accessory);
return view('accessories/checkin', compact('accessory', 'target_option'))->with('backto', $backto);
return view('accessories/checkin', compact('accessory'))->with('backto', $backto);
}
/**
@@ -58,14 +51,8 @@ class AccessoryCheckinController extends Controller
$accessory = Accessory::find($accessory_checkout->accessory_id);
session()->put('checkedInFrom', $accessory_checkout->assigned_to);
session()->put('checkout_to_type', match ($accessory_checkout->assigned_type) {
'App\Models\User' => 'user',
'App\Models\Location' => 'location',
'App\Models\Asset' => 'asset',
});
$this->authorize('checkin', $accessory);
$checkin_hours = date('H:i:s');
$checkin_at = date('Y-m-d H:i:s');
if ($request->filled('checkin_at')) {
@@ -78,8 +65,7 @@ class AccessoryCheckinController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.checkin.success'));
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))->with('success', trans('admin/accessories/message.checkin.success'));
}
// Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkin.error'));

View File

@@ -75,29 +75,26 @@ class AccessoryCheckoutController extends Controller
$accessory->checkout_qty = $request->input('checkout_qty', 1);
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory_checkout = new AccessoryCheckout([
AccessoryCheckout::create([
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'created_by' => auth()->id(),
'assigned_to' => $target->id,
'assigned_type' => $target::class,
'note' => $request->input('note'),
]);
$accessory_checkout->created_by = auth()->id();
$accessory_checkout->save();
}
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
// Set this as user since we only allow checkout to user for this item type
$request->request->add(['checkout_to_type' => request('checkout_to_type')]);
$request->request->add(['assigned_to' => $target->id]);
$request->request->add(['assigned_user' => $target->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
// Redirect to the new accessory page
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
return redirect()->to(Helper::getRedirectOption($request, $accessory->id, 'Accessories'))
->with('success', trans('admin/accessories/message.checkout.success'));
}
}

View File

@@ -7,7 +7,6 @@ use App\Events\CheckoutDeclined;
use App\Events\ItemAccepted;
use App\Events\ItemDeclined;
use App\Http\Controllers\Controller;
use App\Mail\CheckoutAcceptanceResponseMail;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
@@ -21,12 +20,9 @@ use App\Models\License;
use App\Models\Component;
use App\Models\Consumable;
use App\Notifications\AcceptanceAssetAcceptedNotification;
use App\Notifications\AcceptanceAssetAcceptedToUserNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use App\Http\Controllers\SettingsController;
@@ -152,8 +148,6 @@ class AcceptanceController extends Controller
}
}
$assigned_user = User::find($acceptance->assigned_to_id);
// this is horrible
switch($acceptance->checkoutable_type){
case 'App\Models\Asset':
@@ -163,30 +157,35 @@ class AcceptanceController extends Controller
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') {
@@ -209,12 +208,9 @@ class AcceptanceController extends Controller
*/
$branding_settings = SettingsController::getPDFBranding();
$path_logo = "";
// Check for the PDF logo path and use that, otherwise use the regular logo path
if (!is_null($branding_settings->acceptance_pdf_logo)) {
$path_logo = public_path() . '/uploads/' . $branding_settings->acceptance_pdf_logo;
} elseif (!is_null($branding_settings->logo)) {
if (is_null($branding_settings->logo)){
$path_logo = "";
} else {
$path_logo = public_path() . '/uploads/' . $branding_settings->logo;
}
@@ -227,12 +223,11 @@ class AcceptanceController extends Controller
'note' => $request->input('note'),
'check_out_date' => Carbon::parse($acceptance->created_at)->format('Y-m-d'),
'accepted_date' => Carbon::parse($acceptance->accepted_at)->format('Y-m-d'),
'assigned_to' => $assigned_user->present()->fullName,
'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,
'admin' => auth()->user()->present()?->fullName,
];
if ($pdf_view_route!='') {
@@ -242,19 +237,6 @@ class AcceptanceController extends Controller
}
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
// Send the PDF to the signing user
if (($request->input('send_copy') == '1') && ($assigned_user->email !='')) {
// Add the attachment for the signing user into the $data array
$data['file'] = $pdf_filename;
try {
$assigned_user->notify(new AcceptanceAssetAcceptedToUserNotification($data));
} catch (\Exception $e) {
Log::warning($e);
}
}
try {
$acceptance->notify(new AcceptanceAssetAcceptedNotification($data));
} catch (\Exception $e) {
@@ -348,29 +330,10 @@ class AcceptanceController extends Controller
$acceptance->decline($sig_filename, $request->input('note'));
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
Log::debug('New event acceptance.');
event(new CheckoutDeclined($acceptance));
$return_msg = trans('admin/users/message.declined');
}
if ($acceptance->alert_on_response_id) {
try {
$recipient = User::find($acceptance->alert_on_response_id);
if ($recipient) {
Log::debug('Attempting to send email acceptance.');
Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail(
$acceptance,
$recipient,
$request->input('asset_acceptance') === 'accepted',
));
Log::debug('Send email notification sucess on checkout acceptance response.');
}
} catch (Exception $e) {
Log::error($e->getMessage());
Log::warning($e);
}
}
return redirect()->to('account/accept')->with('success', $return_msg);

View File

@@ -40,13 +40,10 @@ class ActionlogController extends Controller
public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
{
$this->authorize('view', \App\Models\Asset::class);
if (config('filesystems.default') == 's3_private') {
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/'.$filename, now()->addMinutes(5)));
}
$file = config('app.private_uploads').'/eula-pdfs/'.$filename;
if (Storage::exists('private_uploads/eula-pdfs/'.$filename)) {
return response()->download(config('app.private_uploads').'/eula-pdfs/'.$filename);
return response()->download($file);
}
return redirect()->back()->with('error', trans('general.file_does_not_exist'));

View File

@@ -13,7 +13,6 @@ use App\Http\Transformers\SelectlistTransformer;
use App\Models\Accessory;
use App\Models\Company;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
@@ -66,7 +65,7 @@ class AccessoriesController extends Controller
}
if ($request->filled('company_id')) {
$accessories->where('accessories.company_id', '=', $request->input('company_id'));
$accessories->where('company_id', '=', $request->input('company_id'));
}
if ($request->filled('category_id')) {
@@ -185,33 +184,39 @@ class AccessoriesController extends Controller
/**
* Get the list of checkouts for a specific accessory
* Display the specified resource.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @param int $id
* @return | array
* @return \Illuminate\Http\Response
*/
public function checkedout(Request $request, $id)
public function checkedout($id, Request $request)
{
$this->authorize('view', Accessory::class);
$accessory = Accessory::with('lastCheckout')->findOrFail($id);
$offset = request('offset', 0);
$limit = request('limit', 50);
// Total count of all checkouts for this asset
$accessory_checkouts = $accessory->checkouts();
$accessory_checkouts = $accessory->checkouts;
$total = $accessory_checkouts->count();
// Check for search text in the request
if ($request->filled('search')) {
$accessory_checkouts = $accessory_checkouts->TextSearch($request->input('search'));
if ($total < $offset) {
$offset = 0;
}
$total = $accessory_checkouts->count();
$accessory_checkouts = $accessory_checkouts->skip($offset)->take($limit)->get();
$accessory_checkouts = $accessory->checkouts()->skip($offset)->take($limit)->get();
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory_checkouts, $total);
if ($request->filled('search')) {
$accessory_checkouts = $accessory->checkouts()->TextSearch($request->input('search'))
->get();
$total = $accessory_checkouts->count();
}
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory, $accessory_checkouts, $total);
}
@@ -222,7 +227,7 @@ class AccessoriesController extends Controller
* @since [v4.0]
* @param \App\Http\Requests\ImageUploadRequest $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function update(ImageUploadRequest $request, $id)
{
@@ -244,16 +249,16 @@ class AccessoriesController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
$this->authorize('delete', Accessory::class);
$accessory = Accessory::withCount('checkouts as checkouts_count')->findOrFail($id);
$accessory = Accessory::findOrFail($id);
$this->authorize($accessory);
if ($accessory->checkouts_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/general.delete_disabled')));
if ($accessory->hasUsers() > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.assoc_users', ['count'=> $accessory->hasUsers()])));
}
$accessory->delete();
@@ -279,17 +284,14 @@ class AccessoriesController extends Controller
$accessory->checkout_qty = $request->input('checkout_qty', 1);
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory_checkout = new AccessoryCheckout([
AccessoryCheckout::create([
'accessory_id' => $accessory->id,
'created_at' => Carbon::now(),
'created_by' => auth()->id(),
'assigned_to' => $target->id,
'assigned_type' => $target::class,
'note' => $request->input('note'),
]);
$accessory_checkout->created_by = auth()->id();
$accessory_checkout->save();
}
// Set this value to be able to pass the qty through to the event

View File

@@ -0,0 +1,200 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function store(UploadFileRequest $request, $assetId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $asset);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assets')) {
Storage::makeDirectory('private_uploads/assets', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
$asset->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.upload.success')));
}
// We only reach here if no files were included in the POST, so tell the user this
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function list($assetId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($asset->id)) {
$this->authorize('view', $asset);
// Check that there are some uploads on this asset that can be listed
if ($asset->uploads->count() > 0) {
$files = array();
foreach ($asset->uploads as $upload) {
array_push($files, $upload);
}
// Give the list of files back to the user
return response()->json(Helper::formatStandardApiResponse('success', $files, trans('admin/hardware/message.upload.success')));
}
// There are no files.
return response()->json(Helper::formatStandardApiResponse('success', array(), trans('admin/hardware/message.upload.success')));
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error')), 500);
}
/**
* Check for permissions and display the file.
*
* @param int $assetId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function show($assetId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($asset->id)) {
$this->authorize('view', $asset);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assets/'.$log->filename;
Log::debug('Checking for '.$file);
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
// Check the file actually exists on the filesystem
if (! Storage::exists($file)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.does_not_exist', ['id' => $fileId])), 404);
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetId
* @param int $fileId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function destroy($assetId = null, $fileId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
$rel_path = 'private_uploads/assets';
// the asset is valid
if (isset($asset->id)) {
$this->authorize('update', $asset);
// Check for the file
$log = Actionlog::find($fileId);
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
// Delete the record of the file
$log->delete();
// All deleting done - notify the user of success
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.deletefile.success')), 200);
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
}

View File

@@ -4,11 +4,11 @@ namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Transformers\MaintenancesTransformer;
use App\Http\Transformers\AssetMaintenancesTransformer;
use App\Models\Asset;
use App\Models\Maintenance;
use App\Models\AssetMaintenance;
use App\Models\Company;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
@@ -18,13 +18,13 @@ use Illuminate\Http\JsonResponse;
*
* @version v2.0
*/
class MaintenancesController extends Controller
class AssetMaintenancesController extends Controller
{
/**
* Generates the JSON response for asset maintenances listing view.
*
* @see MaintenancesController::getIndex() method that generates view
* @see AssetMaintenancesController::getIndex() method that generates view
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
@@ -33,7 +33,7 @@ class MaintenancesController extends Controller
{
$this->authorize('view', Asset::class);
$maintenances = Maintenance::select('maintenances.*')
$maintenances = AssetMaintenance::select('asset_maintenances.*')
->with('asset', 'asset.model', 'asset.location', 'asset.defaultLoc', 'supplier', 'asset.company', 'asset.assetstatus', 'adminuser');
if ($request->filled('search')) {
@@ -45,11 +45,11 @@ class MaintenancesController extends Controller
}
if ($request->filled('supplier_id')) {
$maintenances->where('maintenances.supplier_id', '=', $request->input('supplier_id'));
$maintenances->where('asset_maintenances.supplier_id', '=', $request->input('supplier_id'));
}
if ($request->filled('created_by')) {
$maintenances->where('maintenances.created_by', '=', $request->input('created_by'));
$maintenances->where('asset_maintenances.created_by', '=', $request->input('created_by'));
}
if ($request->filled('asset_maintenance_type')) {
@@ -63,7 +63,7 @@ class MaintenancesController extends Controller
$allowed_columns = [
'id',
'name',
'title',
'asset_maintenance_time',
'asset_maintenance_type',
'cost',
@@ -75,7 +75,6 @@ class MaintenancesController extends Controller
'serial',
'created_by',
'supplier',
'location',
'is_warranty',
'status_label',
];
@@ -99,9 +98,6 @@ class MaintenancesController extends Controller
case 'serial':
$maintenances = $maintenances->OrderByAssetSerial($order);
break;
case 'location':
$maintenances = $maintenances->OrderLocationName($order);
break;
case 'status_label':
$maintenances = $maintenances->OrderStatusName($order);
break;
@@ -112,7 +108,7 @@ class MaintenancesController extends Controller
$total = $maintenances->count();
$maintenances = $maintenances->skip($offset)->take($limit)->get();
return (new MaintenancesTransformer())->transformMaintenances($maintenances, $total);
return (new AssetMaintenancesTransformer())->transformAssetMaintenances($maintenances, $total);
}
@@ -121,23 +117,22 @@ class MaintenancesController extends Controller
/**
* Validates and stores the new asset maintenance
*
* @see MaintenancesController::getCreate() method for the form
* @see AssetMaintenancesController::getCreate() method for the form
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
*/
public function store(ImageUploadRequest $request) : JsonResponse | array
public function store(Request $request) : JsonResponse
{
$this->authorize('update', Asset::class);
// create a new model instance
$maintenance = new Maintenance();
$maintenance = new AssetMaintenance();
$maintenance->fill($request->all());
$maintenance->created_by = auth()->id();
$maintenance = $request->handleImages($maintenance);
// Was the asset maintenance created?
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.create.success')));
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.create.success')));
}
@@ -154,15 +149,15 @@ class MaintenancesController extends Controller
* @version v1.0
* @since [v4.0]
*/
public function update(Request $request, $id) : JsonResponse | array
public function update(Request $request, $id) : JsonResponse
{
$this->authorize('update', Asset::class);
if ($maintenance = Maintenance::with('asset')->find($id)) {
if ($maintenance = AssetMaintenance::with('asset')->find($id)) {
// Can this user manage this asset?
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.action_permission_denied', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id, 'action' => trans('general.edit')])));
}
// The asset this miantenance is attached to is not valid or has been deleted
@@ -173,13 +168,13 @@ class MaintenancesController extends Controller
$maintenance->fill($request->all());
if ($maintenance->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.edit.success')));
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/asset_maintenances/message.edit.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $maintenance->getErrors()));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/maintenances/general.maintenance'), 'id' => $id])));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.item_not_found', ['item_type' => trans('admin/asset_maintenances/general.maintenance'), 'id' => $id])));
}
@@ -187,20 +182,20 @@ class MaintenancesController extends Controller
* Delete an asset maintenance
*
* @author A. Gianotto <snipe@snipe.net>
* @param int $maintenanceId
* @param int $assetMaintenanceId
* @version v1.0
* @since [v4.0]
*/
public function destroy($maintenanceId) : JsonResponse | array
public function destroy($assetMaintenanceId) : JsonResponse
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
$maintenance = Maintenance::findOrFail($maintenanceId);
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
$maintenance->delete();
$assetMaintenance->delete();
return response()->json(Helper::formatStandardApiResponse('success', $maintenance, trans('admin/maintenances/message.delete.success')));
return response()->json(Helper::formatStandardApiResponse('success', $assetMaintenance, trans('admin/asset_maintenances/message.delete.success')));
}
@@ -209,19 +204,19 @@ class MaintenancesController extends Controller
* View an asset maintenance
*
* @author A. Gianotto <snipe@snipe.net>
* @param int $maintenanceId
* @param int $assetMaintenanceId
* @version v1.0
* @since [v4.0]
*/
public function show($maintenanceId) : JsonResponse | array
public function show($assetMaintenanceId) : JsonResponse
{
$this->authorize('view', Asset::class);
$maintenance = Maintenance::findOrFail($maintenanceId);
if (! Company::isCurrentUserHasAccess($maintenance->asset)) {
$assetMaintenance = AssetMaintenance::findOrFail($assetMaintenanceId);
if (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot view a maintenance for that asset'));
}
return (new MaintenancesTransformer())->transformMaintenance($maintenance);
return (new AssetMaintenancesTransformer())->transformAssetMaintenance($assetMaintenance);
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\AssetModel;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetModelFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetModelId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function store(UploadFileRequest $request, $assetModelId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $assetModel);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assetmodels')) {
Storage::makeDirectory('private_uploads/assetmodels', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$assetModel->id, $file);
$assetModel->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $assetModel, trans('admin/models/message.upload.success')));
}
// We only reach here if no files were included in the POST, so tell the user this
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetModelId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function list($assetModelId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('view', $assetModel);
// Check that there are some uploads on this asset that can be listed
if ($assetModel->uploads->count() > 0) {
$files = array();
foreach ($assetModel->uploads as $upload) {
array_push($files, $upload);
}
// Give the list of files back to the user
return response()->json(Helper::formatStandardApiResponse('success', $files, trans('admin/models/message.upload.success')));
}
// There are no files.
return response()->json(Helper::formatStandardApiResponse('success', array(), trans('admin/models/message.upload.success')));
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error')), 500);
}
/**
* Check for permissions and display the file.
*
* @param int $assetModelId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v7.0.12]
* @author [r-xyz]
*/
public function show($assetModelId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('view', $assetModel);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $assetModel->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assetmodels/'.$log->filename;
Log::debug('Checking for '.$file);
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
// Check the file actually exists on the filesystem
if (! Storage::exists($file)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.does_not_exist', ['id' => $fileId])), 404);
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetModelId
* @param int $fileId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function destroy($assetModelId = null, $fileId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
$rel_path = 'private_uploads/assetmodels';
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('update', $assetModel);
// Check for the file
$log = Actionlog::find($fileId);
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
// Delete the record of the file
$log->delete();
// All deleting done - notify the user of success
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/models/message.deletefile.success')), 200);
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
}

View File

@@ -77,24 +77,6 @@ class AssetModelsController extends Controller
$assetmodels->onlyTrashed();
}
if ($request->filled('name')) {
$assetmodels = $assetmodels->where('models.name', '=', $request->input('name'));
}
if ($request->filled('model_number')) {
$assetmodels = $assetmodels->where('models.model_number', '=', $request->input('model_number'));
}
if ($request->input('requestable') == 'true') {
$assetmodels = $assetmodels->where('models.requestable', '=', '1');
} elseif ($request->input('requestable') == 'false') {
$assetmodels = $assetmodels->where('models.requestable', '=', '0');
}
if ($request->filled('notes')) {
$assetmodels = $assetmodels->where('models.notes', '=', $request->input('notes'));
}
if ($request->filled('category_id')) {
$assetmodels = $assetmodels->where('models.category_id', '=', $request->input('category_id'));
}
@@ -154,7 +136,7 @@ class AssetModelsController extends Controller
$assetmodel = $request->handleImages($assetmodel);
if ($assetmodel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.create.success')));
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
@@ -207,7 +189,7 @@ class AssetModelsController extends Controller
$assetmodel = AssetModel::findOrFail($id);
$assetmodel->fill($request->all());
$assetmodel = $request->handleImages($assetmodel);
/**
* Allow custom_fieldset_id to override and populate fieldset_id.
* This is stupid, but required for legacy API support.
@@ -222,7 +204,7 @@ class AssetModelsController extends Controller
if ($assetmodel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.update.success')));
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.update.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));

View File

@@ -6,7 +6,6 @@ use App\Events\CheckoutableCheckedIn;
use App\Http\Requests\StoreAssetRequest;
use App\Http\Requests\UpdateAssetRequest;
use App\Http\Traits\MigratesLegacyAssetLocations;
use App\Models\AccessoryCheckout;
use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat;
use Illuminate\Database\Eloquent\Builder;
@@ -27,14 +26,15 @@ use App\Models\License;
use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
use App\View\Label;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
/**
@@ -114,28 +114,21 @@ class AssetsController extends Controller
'byod',
'asset_eol_date',
'requestable',
'jobtitle',
];
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
foreach ($all_custom_fields as $field) {
$allowed_columns[] = $field->db_column_name();
}
$filter = [];
if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true);
}
$filter = array_filter($filter, function ($key) use ($allowed_columns) {
return in_array($key, $allowed_columns);
}, ARRAY_FILTER_USE_KEY);
$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(
'model',
'location',
'assetstatus',
'company',
@@ -146,9 +139,8 @@ class AssetsController extends Controller
'model.category',
'model.manufacturer',
'model.fieldset',
'model.depreciation',
'supplier'
); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
); //it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
if ($filter_non_deprecable_assets) {
@@ -305,15 +297,9 @@ class AssetsController extends Controller
if ($request->input('requestable') == 'true') {
$assets->where('assets.requestable', '=', '1');
}
if ($request->filled('model_id')) {
// If model_id is already an array, just use it as-is
if (is_array($request->input('model_id'))) {
$assets->InModelList($request->input('model_id'));
} else {
// Otherwise, turn it into an array
$assets->InModelList([$request->input('model_id')]);
}
$assets->InModelList([$request->input('model_id')]);
}
if ($request->filled('category_id')) {
@@ -402,9 +388,6 @@ class AssetsController extends Controller
case 'assigned_to':
$assets->OrderAssigned($order);
break;
case 'jobtitle':
$assets->OrderByJobTitle($order);
break;
case 'created_by':
$assets->OrderByCreatedByName($order);
break;
@@ -452,6 +435,12 @@ class AssetsController extends Controller
}]);
}
/**
* Here we're just determining which Transformer (via $transformer) to use based on the
* variables we set earlier on in this method - we default to AssetsTransformer.
*/
return (new $transformer)->transformAssets($assets, $total, $request);
}
@@ -502,32 +491,15 @@ class AssetsController extends Controller
public function showBySerial(Request $request, $serial): JsonResponse | array
{
$this->authorize('index', Asset::class);
$assets = Asset::where('serial', $serial)->with([
'assetstatus',
'assignedTo',
'company',
'defaultLoc',
'location',
'model.category',
'model.depreciation',
'model.fieldset',
'model.manufacturer',
'supplier',
]);
$assets = Asset::where('serial', $serial)->with('assetstatus')->with('assignedTo');
// Check if they've passed ?deleted=true
if ($request->input('deleted', 'false') == 'true') {
$assets = $assets->withTrashed();
}
$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();
if (($assets) && ($assets->count()) > 0) {
return (new AssetsTransformer)->transformAssets($assets, $total);
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
@@ -584,12 +556,7 @@ class AssetsController extends Controller
'assets.assigned_to',
'assets.assigned_type',
'assets.status_id',
])->with('model', 'assetstatus', 'assignedTo')
->NotArchived();
if ((Setting::getSettings()->full_multiple_companies_support=='1') && ($request->filled('companyId'))) {
$assets->where('assets.company_id', $request->input('companyId'));
}
])->with('model', 'assetstatus', 'assignedTo')->NotArchived();
if ($request->filled('assetStatusType') && $request->input('assetStatusType') === 'RTD') {
$assets = $assets->RTD();
@@ -599,6 +566,7 @@ class AssetsController extends Controller
$assets = $assets->AssignedSearch($request->input('search'));
}
$assets = $assets->paginate(50);
// Loop through and set some custom properties for the transformer to use.
@@ -711,9 +679,7 @@ class AssetsController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.create.success')));
// below is what we want the _eventual_ return to look like - in a more standardized format.
// return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
@@ -799,13 +765,9 @@ class AssetsController extends Controller
}
if ($problems_updating_encrypted_custom_fields) {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning')));
// Below is the *correct* return since it uses the transformer, but we have to use the old, flat return for now until we can update Jamf2Snipe and Kanji2Snipe
// return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.encrypted_warning')));
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.encrypted_warning')));
} else {
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success')));
// Below is the *correct* return since it uses the transformer, but we have to use the old, flat return for now until we can update Jamf2Snipe and Kanji2Snipe
/// return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($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);
@@ -1081,7 +1043,7 @@ class AssetsController extends Controller
* @param int $id
* @since [v4.0]
*/
public function audit(Request $request, Asset $asset): JsonResponse
public function audit(Request $request): JsonResponse
{
$this->authorize('audit', Asset::class);
@@ -1089,15 +1051,36 @@ class AssetsController extends Controller
$settings = Setting::getSettings();
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
// Allow the asset tag to be passed in the payload (legacy method)
if ($request->filled('asset_tag')) {
$asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first();
// No tag passed - return an error
if (!$request->filled('asset_tag')) {
return response()->json(Helper::formatStandardApiResponse('error', [
'asset_tag' => '',
'error' => trans('admin/hardware/message.no_tag'),
], trans('admin/hardware/message.no_tag')), 200);
}
$asset = Asset::where('asset_tag', '=', $request->input('asset_tag'))->first();
if ($asset) {
$originalValues = $asset->getRawOriginal();
/**
* Even though we do a save() further down, we don't want to log this as a "normal" asset update,
* which would trigger the Asset Observer and would log an asset *update* log entry (because the
* de-normed fields like next_audit_date on the asset itself will change on save()) *in addition* to
* the audit log entry we're creating through this controller.
*
* To prevent this double-logging (one for update and one for audit), we skip the observer and bypass
* that de-normed update log entry by using unsetEventDispatcher(), BUT invoking unsetEventDispatcher()
* will bypass normal model-level validation that's usually handled at the observer )
*
* We handle validation on the save() by checking if the asset is valid via the ->isValid() method,
* which manually invokes Watson Validating to make sure the asset's model is valid.
*
* @see \App\Observers\AssetObserver::updating()
*/
$asset->unsetEventDispatcher();
$asset->next_audit_date = $dt;
if ($request->filled('next_audit_date')) {
@@ -1112,89 +1095,33 @@ class AssetsController extends Controller
$asset->last_audit_date = date('Y-m-d H:i:s');
// Set up the payload for re-display in the API response
$payload = [
'id' => $asset->id,
'asset_tag' => $asset->asset_tag,
'note' => $request->input('note'),
'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date),
];
/**
* Update custom fields in the database.
* Validation for these fields is handled through the AssetRequest form request
* $model = AssetModel::find($request->get('model_id'));
*/
if (($asset->model) && ($asset->model->fieldset)) {
$payload['custom_fields'] = [];
foreach ($asset->model->fieldset->fields as $field) {
if (($field->display_audit=='1') && ($request->has($field->db_column))) {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
}
$payload['custom_fields'][$field->db_column] = $request->input($field->db_column);
}
}
}
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
// Validate the rest of the data before we turn off the event dispatcher
if ($asset->isInvalid()) {
return response()->json(Helper::formatStandardApiResponse('error', ['asset_tag' => $asset->asset_tag], $asset->getErrors()));
}
/**
* Even though we do a save() further down, we don't want to log this as a "normal" asset update,
* which would trigger the Asset Observer and would log an asset *update* log entry (because the
* de-normed fields like next_audit_date on the asset itself will change on save()) *in addition* to
* the audit log entry we're creating through this controller.
*
* To prevent this double-logging (one for update and one for audit), we skip the observer and bypass
* that de-normed update log entry by using unsetEventDispatcher(), BUT invoking unsetEventDispatcher()
* will bypass normal model-level validation that's usually handled at the observer)
*
* We handle validation on the save() by checking if the asset is valid via the ->isValid() method,
* which manually invokes Watson Validating to make sure the asset's model is valid.
*
* @see \App\Observers\AssetObserver::updating()
* @see \App\Models\Asset::save()
*/
$asset->unsetEventDispatcher();
/**
* Invoke Watson Validating to check the asset itself and check to make sure it saved correctly.
* We have to invoke this manually because of the unsetEventDispatcher() above.)
*/
if ($asset->isValid() && $asset->save()) {
$asset->logAudit(request('note'), request('location_id'), null, $originalValues);
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/hardware/message.audit.success')));
$asset->logAudit(request('note'), request('location_id'));
return response()->json(Helper::formatStandardApiResponse('success', [
'asset_tag' => e($asset->asset_tag),
'note' => e($request->input('note')),
'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date),
], trans('admin/hardware/message.audit.success')));
}
// Asset failed validation or was not able to be saved
return response()->json(Helper::formatStandardApiResponse('error', [
'asset_tag' => e($asset->asset_tag),
'error' => $asset->getErrors()->first(),
], trans('admin/hardware/message.audit.error', ['error' => $asset->getErrors()->first()])), 200);
}
// No matching asset for the asset tag that was passed.
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
return response()->json(Helper::formatStandardApiResponse('error', [
'asset_tag' => e($request->input('asset_tag')),
'error' => trans('admin/hardware/message.audit.error'),
], trans('admin/hardware/message.audit.error', ['error' => trans('admin/hardware/message.does_not_exist')])), 200);
}
@@ -1287,32 +1214,6 @@ class AssetsController extends Controller
return (new AssetsTransformer)->transformRequestedAssets($assets, $total);
}
public function assignedAssets(Request $request, Asset $asset) : JsonResponse | array
{
return [];
// to do
}
public function assignedAccessories(Request $request, Asset $asset) : JsonResponse | array
{
$this->authorize('view', Asset::class);
$this->authorize('view', $asset);
$accessory_checkouts = AccessoryCheckout::AssetsAssigned()
->where('assigned_to', $asset->id)
->with('adminuser')
->with('accessories');
$offset = ($request->input('offset') > $accessory_checkouts->count()) ? $accessory_checkouts->count() : app('api_offset_value');
$limit = app('api_limit_value');
$total = $accessory_checkouts->count();
$accessory_checkouts = $accessory_checkouts->skip($offset)->take($limit)->get();
return (new AssetsTransformer)->transformCheckedoutAccessories($accessory_checkouts, $total);
}
/**
* Generate asset labels by tag
*

View File

@@ -39,7 +39,6 @@ class CategoriesController extends Controller
'components_count',
'licenses_count',
'image',
'notes',
];
$categories = Category::select([
@@ -53,10 +52,9 @@ class CategoriesController extends Controller
'require_acceptance',
'checkin_email',
'image',
'notes',
])
->with('adminuser')
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count');
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count');
/*
@@ -212,7 +210,7 @@ class CategoriesController extends Controller
public function destroy($id) : JsonResponse
{
$this->authorize('delete', Category::class);
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($id);
$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);
if (! $category->isDeletable()) {
return response()->json(

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\CheckoutRequests\CancelCheckoutRequestAction;
use App\Actions\CheckoutRequests\CreateCheckoutRequestAction;
use App\Exceptions\AssetNotRequestable;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Exception;
class CheckoutRequest extends Controller
{
public function store(Asset $asset): JsonResponse
{
try {
CreateCheckoutRequestAction::run($asset, auth()->user());
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.requests.success')));
} catch (AssetNotRequestable $e) {
return response()->json(Helper::formatStandardApiResponse('error', 'Asset is not requestable'));
} catch (AuthorizationException $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.insufficient_permissions')));
} catch (Exception $e) {
report($e);
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
}
}
public function destroy(Asset $asset): JsonResponse
{
try {
CancelCheckoutRequestAction::run($asset, auth()->user());
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.requests.canceled')));
} catch (AuthorizationException $e) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.insufficient_permissions')));
} catch (Exception $e) {
report($e);
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong')));
}
}
}

View File

@@ -38,15 +38,11 @@ class CompaniesController extends Controller
'accessories_count',
'consumables_count',
'components_count',
'notes',
];
$companies = Company::withCount(['assets as assets_count' => function ($query) {
$query->AssetsForShow();
}])
->with('adminuser')
->withCount('licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'users as users_count');
}])->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'));
@@ -122,7 +118,6 @@ class CompaniesController extends Controller
{
$this->authorize('view', Company::class);
$company = Company::findOrFail($id);
$this->authorize('view', $company);
return (new CompaniesTransformer)->transformCompany($company);
}
@@ -140,7 +135,6 @@ class CompaniesController extends Controller
{
$this->authorize('update', Company::class);
$company = Company::findOrFail($id);
$this->authorize('update', $company);
$company->fill($request->all());
$company = $request->handleImages($company);
@@ -193,7 +187,6 @@ class CompaniesController extends Controller
'companies.image',
]);
if ($request->filled('search')) {
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->get('search').'%');
}

View File

@@ -48,8 +48,7 @@ class ComponentsController extends Controller
];
$components = Component::select('components.*')
->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer', 'uncontrainedAssets')
->withSum('uncontrainedAssets', 'components_assets.assigned_qty');
->with('company', 'location', 'category', 'assets', 'supplier', 'adminuser', 'manufacturer');
if ($request->filled('search')) {
$components = $components->TextSearch($request->input('search'));
@@ -60,7 +59,7 @@ class ComponentsController extends Controller
}
if ($request->filled('company_id')) {
$components->where('components.company_id', '=', $request->input('company_id'));
$components->where('company_id', '=', $request->input('company_id'));
}
if ($request->filled('category_id')) {
@@ -198,11 +197,6 @@ class ComponentsController extends Controller
$this->authorize('delete', Component::class);
$component = Component::findOrFail($id);
$this->authorize('delete', $component);
if ($component->numCheckedOut() > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.delete.error_qty')));
}
$component->delete();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.delete.success')));

View File

@@ -40,7 +40,7 @@ class ConsumablesController extends Controller
}
if ($request->filled('company_id')) {
$consumables->where('consumables.company_id', '=', $request->input('company_id'));
$consumables->where('company_id', '=', $request->input('company_id'));
}
if ($request->filled('category_id')) {
@@ -228,16 +228,11 @@ class ConsumablesController extends Controller
foreach ($consumable->consumableAssignments as $consumable_assignment) {
$rows[] = [
'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '',
'user' => ($consumable_assignment->user) ? [
'id' => (int) $consumable_assignment->user->id,
'name'=> e($consumable_assignment->user->present()->fullName()),
] : null,
'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,
'created_by' => ($consumable_assignment->adminuser) ? [
'id' => (int) $consumable_assignment->adminuser->id,
'name'=> e($consumable_assignment->adminuser->present()->fullName()),
] : null,
'admin' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null, // legacy, so we don't change the shape of the response
'created_by' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null,
];
}

View File

@@ -23,7 +23,7 @@ class DepartmentsController extends Controller
public function index(Request $request) : JsonResponse | array
{
$this->authorize('view', Department::class);
$allowed_columns = ['id', 'name', 'image', 'users_count', 'notes'];
$allowed_columns = ['id', 'name', 'image', 'users_count'];
$departments = Department::select(
'departments.id',
@@ -35,8 +35,7 @@ class DepartmentsController extends Controller
'departments.manager_id',
'departments.created_at',
'departments.updated_at',
'departments.image',
'departments.notes',
'departments.image'
)->with('users')->with('location')->with('manager')->with('company')->withCount('users as users_count');
if ($request->filled('search')) {
@@ -73,9 +72,6 @@ class DepartmentsController extends Controller
case 'manager':
$departments->OrderManager($order);
break;
case 'company':
$departments->OrderCompany($order);
break;
default:
$departments->orderBy($sort, $order);
break;

View File

@@ -24,7 +24,7 @@ class GroupsController extends Controller
$this->authorize('view', Group::class);
$groups = Group::select('id', 'name', 'permissions', 'notes', 'created_at', 'updated_at', 'created_by')->with('adminuser')->withCount('users as users_count');
$groups = Group::select('id', 'name', 'permissions', 'created_at', 'updated_at', 'created_by')->with('adminuser')->withCount('users as users_count');
if ($request->filled('search')) {
$groups = $groups->TextSearch($request->input('search'));
@@ -76,12 +76,11 @@ class GroupsController extends Controller
$this->authorize('superadmin');
$group = new Group;
// Get all the available permissions
$permissions = json_encode(config('permissions'));
$permissions = config('permissions');
$groupPermissions = Helper::selectedPermissionsArray($permissions, $permissions);
$group->name = $request->input('name');
$group->created_by = auth()->id();
$group->notes = $request->input('notes');
$group->permissions = json_encode($request->input('permissions', $groupPermissions));
if ($group->save()) {
@@ -119,7 +118,6 @@ class GroupsController extends Controller
$group = Group::findOrFail($id);
$group->name = $request->input('name');
$group->notes = $request->input('notes');
$group->permissions = $request->input('permissions'); // Todo - some JSON validation stuff here
if ($group->save()) {

View File

@@ -9,14 +9,12 @@ use App\Http\Transformers\ImportsTransformer;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Import;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Database\Eloquent\JsonEncodingException;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use League\Csv\Reader;
use Onnov\DetectEncoding\EncodingDetector;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\JsonResponse;
@@ -47,8 +45,6 @@ class ImportController extends Controller
$path = config('app.private_uploads').'/imports';
$results = [];
$import = new Import;
$detector = new EncodingDetector();
foreach ($files as $file) {
if (! in_array($file->getMimeType(), [
'application/vnd.ms-excel',
@@ -59,6 +55,7 @@ class ImportController extends Controller
'text/comma-separated-values',
'text/tsv', ])) {
$results['error'] = 'File type must be CSV. Uploaded file is '.$file->getMimeType();
return response()->json(Helper::formatStandardApiResponse('error', null, $results['error']), 422);
}
@@ -66,40 +63,6 @@ class ImportController extends Controller
if (! ini_get('auto_detect_line_endings')) {
ini_set('auto_detect_line_endings', '1');
}
if (function_exists('iconv')) {
$file_contents = $file->getContent(); //TODO - this *does* load the whole file in RAM, but we need that to be able to 'iconv' it?
$encoding = $detector->getEncoding($file_contents);
\Log::warning("Discovered encoding: $encoding in uploaded CSV");
$reader = null;
if (strcasecmp($encoding, 'UTF-8') != 0) {
$transliterated = false;
try {
$transliterated = iconv(strtoupper($encoding), 'UTF-8', $file_contents);
} catch (\Exception $e) {
$transliterated = false; //blank out the partially-decoded string
return response()->json(
Helper::formatStandardApiResponse(
'error',
null,
trans('admin/hardware/message.import.transliterate_failure', ["encoding" => $encoding])
),
422
);
}
if ($transliterated !== false) {
$tmpname = tempnam(sys_get_temp_dir(), '');
$tmpresults = file_put_contents($tmpname, $transliterated);
$transliterated = null; //save on memory?
if ($tmpresults !== false) {
$newfile = new UploadedFile($tmpname, $file->getClientOriginalName(), null, null, true); //WARNING: this is enabling 'test mode' - which is gross, but otherwise the file won't be treated as 'uploaded'
if ($newfile->isValid()) {
$file = $newfile;
}
}
}
}
$file_contents = null; //try to save on memory, I guess?
}
$reader = Reader::createFromFileObject($file->openFile('r')); //file pointer leak?
try {
@@ -234,15 +197,6 @@ class ImportController extends Controller
case 'location':
$redirectTo = 'locations.index';
break;
case 'supplier':
$redirectTo = 'suppliers.index';
break;
case 'manufacturer':
$redirectTo = 'manufacturers.index';
break;
case 'category':
$redirectTo = 'categories.index';
break;
}
if ($errors) { //Failure

View File

@@ -29,21 +29,12 @@ class LicenseSeatsController extends Controller
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
->where('license_seats.license_id', $licenseId);
if ($request->input('status') == 'available') {
$seats->whereNull('license_seats.assigned_to');
}
if ($request->input('status') == 'assigned') {
$seats->ByAssigned();
}
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
if ($request->input('sort') == 'department') {
$seats->OrderDepartments($order);
} else {
$seats->orderBy('updated_at', $order);
$seats->orderBy('id', $order);
}
$total = $seats->count();
@@ -145,13 +136,13 @@ class LicenseSeatsController extends Controller
if ($licenseSeat->save()) {
if ($is_checkin) {
$licenseSeat->logCheckin($target, $request->input('notes'));
$licenseSeat->logCheckin($target, $request->input('note'));
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
// in this case, relevant fields are touched but it's not a checkin operation. so it must be a checkout operation.
$licenseSeat->logCheckout($request->input('notes'), $target);
$licenseSeat->logCheckout($request->input('note'), $target);
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}

View File

@@ -3,22 +3,17 @@
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Transformers\AccessoriesTransformer;
use App\Http\Controllers\Controller;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\LocationsTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Location;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Http\JsonResponse;
class LocationsController extends Controller
{
@@ -33,30 +28,26 @@ class LocationsController extends Controller
{
$this->authorize('view', Location::class);
$allowed_columns = [
'accessories_count',
'id',
'name',
'address',
'address2',
'assets_count',
'assets_count',
'assigned_accessories_count',
'assigned_assets_count',
'assigned_assets_count',
'city',
'country',
'created_at',
'currency',
'id',
'image',
'ldap_ou',
'company_id',
'manager_id',
'name',
'rtd_assets_count',
'state',
'updated_at',
'users_count',
'country',
'zip',
'notes',
'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',
];
$locations = Location::with('parent', 'manager', 'children')->select([
@@ -77,23 +68,11 @@ class LocationsController extends Controller
'locations.image',
'locations.ldap_ou',
'locations.currency',
'locations.company_id',
'locations.notes',
])
->withCount('assignedAssets as assigned_assets_count')
->withCount('assignedAssets as assigned_assets_count')
])->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('assignedAccessories as assigned_accessories_count')
->withCount('accessories as accessories_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->with('adminuser');
// Only scope locations if the setting is enabled
if (Setting::getSettings()->scope_locations_fmcs) {
$locations = Company::scopeCompanyables($locations);
}
->withCount('users as users_count');
if ($request->filled('search')) {
$locations = $locations->TextSearch($request->input('search'));
@@ -127,10 +106,6 @@ class LocationsController extends Controller
$locations->where('locations.manager_id', '=', $request->input('manager_id'));
}
if ($request->filled('company_id')) {
$locations->where('locations.company_id', '=', $request->input('company_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');
@@ -147,9 +122,6 @@ class LocationsController extends Controller
case 'manager':
$locations->OrderManager($order);
break;
case 'company':
$locations->OrderCompany($order);
break;
default:
$locations->orderBy($sort, $order);
break;
@@ -177,15 +149,6 @@ class LocationsController extends Controller
$location->fill($request->all());
$location = $request->handleImages($location);
// Only scope location if the setting is enabled
if (Setting::getSettings()->scope_locations_fmcs) {
$location->company_id = Company::getIdForCurrentUser($request->get('company_id'));
// check if parent is set and has a different company
if ($location->parent_id && Location::find($location->parent_id)->company_id != $location->company_id) {
response()->json(Helper::formatStandardApiResponse('error', null, 'different company than parent'));
}
}
if ($location->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new LocationsTransformer)->transformLocation($location), trans('admin/locations/message.create.success')));
}
@@ -203,7 +166,7 @@ class LocationsController extends Controller
public function show($id) : JsonResponse | array
{
$this->authorize('view', Location::class);
$location = Location::with('parent', 'manager', 'children', 'company')
$location = Location::with('parent', 'manager', 'children')
->select([
'locations.id',
'locations.name',
@@ -219,8 +182,6 @@ class LocationsController extends Controller
'locations.updated_at',
'locations.image',
'locations.currency',
'locations.company_id',
'locations.notes',
])
->withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
@@ -248,19 +209,6 @@ class LocationsController extends Controller
$location->fill($request->all());
$location = $request->handleImages($location);
if ($request->filled('company_id')) {
// Only scope location if the setting is enabled
if (Setting::getSettings()->scope_locations_fmcs) {
$location->company_id = Company::getIdForCurrentUser($request->get('company_id'));
// check if there are related objects with different company
if (Helper::test_locations_fmcs(false, $id, $location->company_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'error scoped locations'));
}
} else {
$location->company_id = $request->get('company_id');
}
}
if ($location->isValid()) {
$location->save();
@@ -276,17 +224,7 @@ class LocationsController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, $location->getErrors()));
}
public function assets(Request $request, Location $location) : JsonResponse | array
{
$this->authorize('view', Asset::class);
$this->authorize('view', $location);
$assets = Asset::where('location_id', '=', $location->id)->with('model', 'model.category', 'assetstatus', 'location', 'company', 'defaultLoc');
$assets = $assets->get();
return (new AssetsTransformer)->transformAssets($assets, $assets->count(), $request);
}
public function assignedAssets(Request $request, Location $location) : JsonResponse | array
{
$this->authorize('view', Asset::class);
$this->authorize('view', $location);
@@ -295,20 +233,6 @@ class LocationsController extends Controller
return (new AssetsTransformer)->transformAssets($assets, $assets->count(), $request);
}
public function assignedAccessories(Request $request, Location $location) : JsonResponse | array
{
$this->authorize('view', Accessory::class);
$this->authorize('view', $location);
$accessory_checkouts = AccessoryCheckout::LocationAssigned()->where('assigned_to', $location->id)->with('adminuser')->with('accessories');
$offset = ($request->input('offset') > $accessory_checkouts->count()) ? $accessory_checkouts->count() : app('api_offset_value');
$limit = app('api_limit_value');
$total = $accessory_checkouts->count();
$accessory_checkouts = $accessory_checkouts->skip($offset)->take($limit)->get();
return (new LocationsTransformer)->transformCheckedoutAccessories($accessory_checkouts, $total);
}
/**
* Remove the specified resource from storage.
*
@@ -381,11 +305,6 @@ class LocationsController extends Controller
'locations.image',
]);
// Only scope locations if the setting is enabled
if (Setting::getSettings()->scope_locations_fmcs) {
$locations = Company::scopeCompanyables($locations);
}
$page = 1;
if ($request->filled('page')) {
$page = $request->input('page');

View File

@@ -39,8 +39,7 @@ class ManufacturersController extends Controller
'assets_count',
'consumables_count',
'components_count',
'licenses_count',
'notes',
'licenses_count'
];
$manufacturers = Manufacturer::select([
@@ -56,7 +55,6 @@ class ManufacturersController extends Controller
'updated_at',
'image',
'deleted_at',
'notes',
])
->with('adminuser')
->withCount('assets as assets_count')

View File

@@ -4,19 +4,15 @@ namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\ProfileTransformer;
use App\Models\CheckoutRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Laravel\Passport\TokenRepository;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Facades\Gate;
use App\Models\CustomField;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ProfileController extends Controller
{
@@ -171,22 +167,6 @@ class ProfileController extends Controller
}
/**
* Display the EULAs accepted by the user.
*
* @param \App\Http\Transformers\ActionlogsTransformer $transformer
* @return \Illuminate\Http\JsonResponse
*@since [v8.1.16]
* @author [Godfrey Martinez] [<gmartinez@grokability.com>]
*/
public function eulas(ProfileTransformer $transformer)
{
// Only return this user's EULAs
$eulas = auth()->user()->eulas;
return response()->json(
$transformer->transformFiles($eulas, $eulas->count())
);
}
}

View File

@@ -5,8 +5,6 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Transformers\ActionlogsTransformer;
use App\Models\Actionlog;
use App\Models\Company;
use App\Models\Setting;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
@@ -20,11 +18,10 @@ class ReportsController extends Controller
*/
public function index(Request $request) : JsonResponse | array
{
$this->authorize('activity.view');
$this->authorize('reports.view');
$actionlogs = Actionlog::with('item', 'user', 'adminuser', 'target', 'location');
if ($request->filled('search')) {
$actionlogs = $actionlogs->TextSearch(e($request->input('search')));
}

View File

@@ -150,11 +150,8 @@ class SettingsController extends Controller
if (!config('app.lock_passwords')) {
try {
Notification::send(Setting::first(), new MailTest());
Log::debug('Attempting to sending to '.config('mail.reply_to.address'));
return response()->json(['message' => 'Mail sent to '.config('mail.reply_to.address')], 200);
} catch (\Exception $e) {
Log::error('Mail sent error using '.config('mail.reply_to.address') .': '. $e->getMessage());
Log::debug($e);
return response()->json(['message' => $e->getMessage()], 500);
}
}
@@ -318,4 +315,4 @@ class SettingsController extends Controller
}
}
}

View File

@@ -290,12 +290,10 @@ class StatuslabelsController extends Controller
/**
* Returns a boolean response based on whether the status label
* is one that is deployable or pending.
* is one that is deployable.
*
* This is used by the hardware create/edit view to determine whether
* we should provide a dropdown of users for them to check the asset out to,
* and whether we show a warning that the asset will be checked in if it's already
* assigned but the status is changed to one that isn't pending or deployable
* we should provide a dropdown of users for them to check the asset out to.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
@@ -303,7 +301,7 @@ class StatuslabelsController extends Controller
public function checkIfDeployable($id) : string
{
$statuslabel = Statuslabel::findOrFail($id);
if (($statuslabel->getStatuslabelType() == 'pending') || ($statuslabel->getStatuslabelType() == 'deployable')) {
if ($statuslabel->getStatuslabelType() == 'deployable') {
return '1';
}

View File

@@ -24,15 +24,10 @@ class SuppliersController extends Controller
public function index(Request $request): array
{
$this->authorize('view', Supplier::class);
$allowed_columns = [
'id',
$allowed_columns = ['
id',
'name',
'address',
'address2',
'city',
'state',
'country',
'zip',
'phone',
'contact',
'fax',
@@ -44,24 +39,21 @@ class SuppliersController extends Controller
'components_count',
'consumables_count',
'url',
'notes',
];
$suppliers = Supplier::select(
['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'created_by', 'updated_at', 'deleted_at', 'image', 'notes', 'url', 'zip'])
['id', 'name', 'address', 'address2', 'city', 'state', 'country', 'fax', 'phone', 'email', 'contact', 'created_at', 'updated_at', 'deleted_at', 'image', 'notes', 'url'])
->withCount('assets as assets_count')
->withCount('licenses as licenses_count')
->withCount('accessories as accessories_count')
->withCount('components as components_count')
->withCount('consumables as consumables_count')
->with('adminuser');
->withCount('consumables as consumables_count');
if ($request->filled('search')) {
$suppliers->TextSearch($request->input('search'));
$suppliers = $suppliers->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$suppliers->where('name', '=', $request->input('name'));
}
@@ -108,15 +100,7 @@ class SuppliersController extends Controller
$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 'created_by':
$suppliers->OrderByCreatedByName($order);
break;
default:
$suppliers->orderBy($sort, $order);
break;
}
$suppliers->orderBy($sort, $order);
$total = $suppliers->count();
$suppliers = $suppliers->skip($offset)->take($limit)->get();
@@ -194,7 +178,7 @@ class SuppliersController extends Controller
public function destroy($id) : JsonResponse
{
$this->authorize('delete', Supplier::class);
$supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id);
$supplier = Supplier::with('asset_maintenances', 'assets', 'licenses')->withCount('asset_maintenances as asset_maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id);
$this->authorize('delete', $supplier);
@@ -202,8 +186,8 @@ class SuppliersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count])));
}
if ($supplier->maintenances_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count])));
if ($supplier->asset_maintenances_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count])));
}
if ($supplier->licenses_count > 0) {

View File

@@ -1,220 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Http\Transformers\UploadedFilesTransformer;
use App\Models\Actionlog;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class UploadedFilesController extends Controller
{
/**
* List files for an object
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to list files for
* @since [v8.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function index(Request $request, $object_type, $id) : JsonResponse | array
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Columns allowed for sorting
$allowed_columns =
[
'id',
'filename',
'action_type',
'action_date',
'note',
'created_at',
];
$uploads = Actionlog::select('action_logs.*')
->whereNotNull('filename')
->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)
->where('action_type', '=', 'uploaded')
->with('adminuser');
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
// Text search on action_logs fields
// We could use the normal Actionlogs text scope, but it's a very heavy query since it's searching across all relations
// and we generally won't need that here
if ($request->filled('search')) {
$uploads->where(
function ($query) use ($request) {
$query->where('filename', 'LIKE', '%' . $request->input('search') . '%')
->orWhere('note', 'LIKE', '%' . $request->input('search') . '%');
}
);
}
$total = $uploads->count();
$uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get();
return (new UploadedFilesTransformer())->transformFiles($uploads, $total);
}
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to store so we can check permisisons
* @since [v8.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, $object_type, $id) : JsonResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// If the file storage directory doesn't exist, create it
if (! Storage::exists(self::$map_storage_path[$object_type])) {
Storage::makeDirectory(self::$map_storage_path[$object_type], 775);
}
if ($request->hasFile('file')) {
// Loop over the attached files and add them to the object
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile(self::$map_storage_path[$object_type], self::$map_file_prefix[$object_type].'-'.$object->id, $file);
$files[] = $file_name;
$object->logUpload($file_name, $request->get('notes'));
}
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')
->where('item_type', '=', self::$map_object_type[$object_type])
->where('item_id', '=', $id)->whereIn('filename', $files)
->get();
return response()->json(Helper::formatStandardApiResponse('success', (new UploadedFilesTransformer())->transformFiles($files, count($files)), trans_choice('general.file_upload_status.upload.success', count($files))));
}
// No files were submitted
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.nofiles')));
}
/**
* Check for permissions and display the file.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to delete from so we can check permisisons
* @param $file_id the ID of the file to delete from the action_logs table
* @since [v8.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function show($object_type, $id, $file_id) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Check that the file being requested exists for the object
if (! $log = Actionlog::whereNotNull('filename')->where('item_type', self::$map_object_type[$object_type])->where('item_id', $object->id)->find($file_id)
) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_id')), 200);
}
if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.file_not_found'), 200));
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download(self::$map_storage_path[$object_type].'/'.$log->filename, $log->filename, $headers);
}
return StorageHelper::downloader(self::$map_storage_path[$object_type].'/'.$log->filename);
}
/**
* Delete the associated file
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to delete from so we can check permisisons
* @param $file_id the ID of the file to delete from the action_logs table
* @since [v8.1.17]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function destroy($object_type, $id, $file_id) : JsonResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Check for the file
$log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)->first();
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
}
// Delete the record of the file
if ($log->delete()) {
return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200);
}
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500);
}
}

View File

@@ -6,7 +6,6 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\SaveUserRequest;
use App\Http\Transformers\AccessoriesTransformer;
use App\Http\Transformers\ActionlogsTransformer;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\ConsumablesTransformer;
use App\Http\Transformers\LicensesTransformer;
@@ -20,11 +19,8 @@ use App\Models\Consumable;
use App\Models\License;
use App\Models\User;
use App\Notifications\CurrentInventory;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
@@ -70,7 +66,6 @@ class UsersController extends Controller
'users.notes',
'users.permissions',
'users.phone',
'users.mobile',
'users.state',
'users.two_factor_enrolled',
'users.two_factor_optin',
@@ -84,22 +79,8 @@ class UsersController extends Controller
'users.autoassign_licenses',
'users.website',
])->with('manager')
->with('groups')
->with('userloc')
->with('company')
->with('department')
->with('createdBy')
->withCount([
'assets as assets_count' => function(Builder $query) {
$query->withoutTrashed();
},
'licenses as licenses_count',
'accessories as accessories_count',
'consumables as consumables_count',
'managesUsers as manages_users_count',
'managedLocations as manages_locations_count'
]);
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations')
->withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'managesUsers as manages_users_count', 'managedLocations as manages_locations_count');
if ($request->filled('search') != '') {
@@ -110,26 +91,10 @@ class UsersController extends Controller
$users = $users->where('users.activated', '=', $request->input('activated'));
}
if ($request->input('admins') == 'true') {
$users = $users->OnlyAdminsAndSuperAdmins();
}
if ($request->input('superadmins') == 'true') {
$users = $users->OnlySuperAdmins();
}
if ($request->filled('company_id')) {
$users = $users->where('users.company_id', '=', $request->input('company_id'));
}
if ($request->filled('phone')) {
$users = $users->where('users.phone', '=', $request->input('phone'));
}
if ($request->filled('mobile')) {
$users = $users->where('users.mobile', '=', $request->input('mobile'));
}
if ($request->filled('location_id')) {
$users = $users->where('users.location_id', '=', $request->input('location_id'));
}
@@ -231,11 +196,11 @@ class UsersController extends Controller
}
if ($request->filled('manages_users_count')) {
$users->has('managesUsers', '=', $request->input('manages_users_count'));
$users->has('manages_users_count', '=', $request->input('manages_users_count'));
}
if ($request->filled('manages_locations_count')) {
$users->has('managedLocations', '=', $request->input('manages_locations_count'));
$users->has('manages_locations_count', '=', $request->input('manages_locations_count'));
}
if ($request->filled('autoassign_licenses')) {
@@ -302,7 +267,6 @@ class UsersController extends Controller
'manages_users_count',
'manages_locations_count',
'phone',
'mobile',
'address',
'city',
'state',
@@ -365,7 +329,6 @@ class UsersController extends Controller
$users = $users->where(function ($query) use ($request) {
$query->SimpleNameSearch($request->get('search'))
->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
->orWhere('email', 'LIKE', '%'.$request->get('search').'%')
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
});
}
@@ -500,25 +463,8 @@ class UsersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
}
// check for permissions related fields and pull them out if the current user cannot edit them
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
if ($request->filled('username')) {
$user->username = $request->input('username');
}
if ($request->filled('email')) {
$user->email = $request->input('email');
}
if ($request->filled('activated')) {
$user->activated = $request->input('activated');
}
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
// We need to use has() instead of filled()
@@ -719,6 +665,7 @@ class UsersController extends Controller
$this->authorize('view', License::class);
if ($user = User::where('id', $id)->withTrashed()->first()) {
$this->authorize('update', $user);
$licenses = $user->licenses()->get();
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
}
@@ -778,25 +725,6 @@ class UsersController extends Controller
return (new UsersTransformer)->transformUser($request->user());
}
/**
* Display the EULAs accepted by the user.
*
* @param \App\Models\User $user
* @param \App\Http\Transformers\ActionlogsTransformer $transformer
* @return \Illuminate\Http\JsonResponse
*@since [v8.1.16]
* @author [Godfrey Martinez] [<gmartinez@grokability.com>]
*/
public function eulas(User $user, ActionlogsTransformer $transformer)
{
$this->authorize('view', User::class);
$eulas = $user->eulas;
return response()->json(
$transformer->transformActionlogs($eulas, $eulas->count())
);
}
/**
* Restore a soft-deleted user.
*
@@ -833,37 +761,4 @@ class UsersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);
}
/**
* Run the LDAP sync command to import users from LDAP via API.
*
* @author A. Gianotto <snipe@snipe.net>
* @since 8.2.2
*
* @return \Illuminate\Http\JsonResponse
*/
public function syncLdapUsers(Request $request)
{
$this->authorize('update', User::class);
// Call Artisan LDAP import command.
Artisan::call('snipeit:ldap-sync', ['--location_id' => $request->input('location_id'), '--json_summary' => true]);
// Collect and parse JSON summary.
$ldap_results_json = Artisan::output();
$ldap_results = json_decode($ldap_results_json, true);
if (!$ldap_results) {
return response()->json(Helper::formatStandardApiResponse('error', null,trans('general.no_results')), 200);
}
// Direct user to appropriate status page.
if ($ldap_results['error']) {
return response()->json(Helper::formatStandardApiResponse('error', null, $ldap_results['error_message']), 200);
}
return response()->json(Helper::formatStandardApiResponse('success', null, $ldap_results['summary']), 200);
}
}

View File

@@ -0,0 +1,289 @@
<?php
namespace App\Http\Controllers;
use App\Models\Asset;
use App\Models\AssetMaintenance;
use App\Models\Company;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Http\Request;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
/**
* This controller handles all actions related to Asset Maintenance for
* the Snipe-IT Asset Management application.
*
* @version v2.0
*/
class AssetMaintenancesController extends Controller
{
/**
* Checks for permissions for this action.
*
* @todo This should be replaced with middleware and/or policies
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
*/
private static function getInsufficientPermissionsRedirect(): RedirectResponse
{
return redirect()->route('maintenances.index')
->with('error', trans('general.insufficient_permissions'));
}
/**
* Returns a view that invokes the ajax tables which actually contains
* the content for the asset maintenances listing, which is generated in getDatatable.
*
* @todo This should be replaced with middleware and/or policies
* @see AssetMaintenancesController::getDatatable() method that generates the JSON response
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
*/
public function index() : View
{
$this->authorize('view', Asset::class);
return view('asset_maintenances/index');
}
/**
* Returns a form view to create a new asset maintenance.
*
* @see AssetMaintenancesController::postCreate() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
* @return mixed
*/
public function create() : View
{
$this->authorize('update', Asset::class);
$asset = null;
if ($asset = Asset::find(request('asset_id'))) {
// We have to set this so that the correct property is set in the select2 ajax dropdown
$asset->asset_id = $asset->id;
}
// Prepare Asset Maintenance Type List
$assetMaintenanceType = [
'' => 'Select an asset maintenance type',
] + AssetMaintenance::getImprovementOptions();
// Mark the selected asset, if it came in
return view('asset_maintenances/edit')
->with('asset', $asset)
->with('assetMaintenanceType', $assetMaintenanceType)
->with('item', new AssetMaintenance);
}
/**
* Validates and stores the new asset maintenance
*
* @see AssetMaintenancesController::getCreate() method for the form
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
*/
public function store(Request $request) : RedirectResponse
{
$this->authorize('update', Asset::class);
// create a new model instance
$assetMaintenance = new AssetMaintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = $request->input('cost');
$assetMaintenance->notes = $request->input('notes');
$asset = Asset::find($request->input('asset_id'));
if ((! Company::isCurrentUserHasAccess($asset)) && ($asset != null)) {
return static::getInsufficientPermissionsRedirect();
}
// Save the asset maintenance data
$assetMaintenance->asset_id = $request->input('asset_id');
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
$assetMaintenance->created_by = auth()->id();
if (($assetMaintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00')
) {
$startDate = Carbon::parse($assetMaintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date);
$assetMaintenance->asset_maintenance_time = $completionDate->diffInDays($startDate);
}
// Was the asset maintenance created?
if ($assetMaintenance->save()) {
// Redirect to the new asset maintenance page
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors());
}
/**
* Returns a form view to edit a selected asset maintenance.
*
* @see AssetMaintenancesController::postEdit() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @param int $assetMaintenanceId
* @version v1.0
* @since [v1.8]
*/
public function edit($assetMaintenanceId = null) : View | RedirectResponse
{
$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');
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
// Prepare Improvement Type List
$assetMaintenanceType = ['' => 'Select an improvement type'] + AssetMaintenance::getImprovementOptions();
return view('asset_maintenances/edit')
->with('selectedAsset', null)
->with('assetMaintenanceType', $assetMaintenanceType)
->with('item', $assetMaintenance);
}
/**
* Validates and stores an update to an asset maintenance
*
* @see AssetMaintenancesController::postEdit() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @param Request $request
* @param int $assetMaintenanceId
* @version v1.0
* @since [v1.8]
*/
public function update(Request $request, $assetMaintenanceId = null) : View | RedirectResponse
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif ((!$assetMaintenance->asset) || ($assetMaintenance->asset->deleted_at!='')) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')->with('error', 'asset does not exist');
} 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->notes = $request->input('notes');
$asset = Asset::find(request('asset_id'));
if (! Company::isCurrentUserHasAccess($asset)) {
return static::getInsufficientPermissionsRedirect();
}
// Save the asset maintenance data
$assetMaintenance->asset_id = $request->input('asset_id');
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title');
$assetMaintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date');
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()) {
// Redirect to the new asset maintenance page
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.edit.success'));
}
return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors());
}
/**
* Delete an asset maintenance
*
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @param int $assetMaintenanceId
* @version v1.0
* @since [v1.8]
*/
public function destroy($assetMaintenanceId) : RedirectResponse
{
$this->authorize('update', Asset::class);
// Check if the asset maintenance exists
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')
->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
// Delete the asset maintenance
$assetMaintenance->delete();
// Redirect to the asset_maintenance management page
return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.delete.success'));
}
/**
* View an asset maintenance
*
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @param int $assetMaintenanceId
* @version v1.0
* @since [v1.8]
*/
public function show($assetMaintenanceId) : View | RedirectResponse
{
$this->authorize('view', Asset::class);
// Check if the asset maintenance exists
if (is_null($assetMaintenance = AssetMaintenance::find($assetMaintenanceId))) {
// Redirect to the asset maintenance management page
return redirect()->route('maintenances.index')
->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif (! Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}
return view('asset_maintenances/view')->with('assetMaintenance', $assetMaintenance);
}
}

View File

@@ -87,20 +87,7 @@ class AssetModelsController extends Controller
$model->fieldset_id = $request->input('fieldset_id');
}
if ($request->has('use_cloned_image')) {
$cloned_model_img = AssetModel::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'models/'.$new_image_name;
Storage::disk('public')->copy('models/'.$cloned_model_img->image, $new_image);
$model->image = $new_image_name;
}
} else {
$model = $request->handleImages($model);
}
$model = $request->handleImages($model);
if ($model->save()) {
if ($this->shouldAddDefaultValues($request->input())) {
@@ -122,11 +109,16 @@ class AssetModelsController extends Controller
* @since [v1.0]
* @param int $modelId
*/
public function edit(AssetModel $model) : View | RedirectResponse
public function edit($modelId = null) : View | RedirectResponse
{
$this->authorize('update', AssetModel::class);
$category_type = 'asset';
return view('models/edit', compact('category_type'))->with('item', $model)->with('depreciation_list', Helper::depreciationList());
if ($item = AssetModel::find($modelId)) {
$category_type = 'asset';
return view('models/edit', compact('item', 'category_type'))->with('depreciation_list', Helper::depreciationList());
}
return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
}
@@ -141,11 +133,16 @@ class AssetModelsController extends Controller
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(StoreAssetModelRequest $request, AssetModel $model) : RedirectResponse
public function update(StoreAssetModelRequest $request, $modelId) : RedirectResponse
{
$this->authorize('update', AssetModel::class);
if (is_null($model = AssetModel::find($modelId))) {
return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
}
$model = $request->handleImages($model);
$model->depreciation_id = $request->input('depreciation_id');
$model->eol = $request->input('eol');
$model->name = $request->input('name');
@@ -191,16 +188,28 @@ class AssetModelsController extends Controller
* @since [v1.0]
* @param int $modelId
*/
public function destroy(AssetModel $model) : RedirectResponse
public function destroy($modelId) : RedirectResponse
{
$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'));
}
if ($model->assets()->count() > 0) {
// Throw an error that this model is associated with assets
return redirect()->route('models.index')->with('error', trans('admin/models/message.assoc_users'));
}
if ($model->image) {
try {
Storage::disk('public')->delete('models/'.$model->image);
$model->update(['image' => null]);
} catch (\Exception $e) {
Log::info($e);
}
}
// Delete the model
$model->delete();
@@ -258,10 +267,16 @@ class AssetModelsController extends Controller
* @since [v1.0]
* @param int $modelId
*/
public function show(AssetModel $model) : View | RedirectResponse
public function show($modelId = null) : View | RedirectResponse
{
$this->authorize('view', AssetModel::class);
return view('models/view', compact('model'));
$model = AssetModel::withTrashed()->find($modelId);
if (isset($model->id)) {
return view('models/view', compact('model'));
}
return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
}
/**
@@ -271,20 +286,23 @@ class AssetModelsController extends Controller
* @since [v1.0]
* @param int $modelId
*/
public function getClone(AssetModel $model) : View | RedirectResponse
public function getClone($modelId = null) : View | RedirectResponse
{
$this->authorize('create', AssetModel::class);
// Check if the model exists
if (is_null($model_to_clone = AssetModel::find($modelId))) {
return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
}
$cloned_model = clone $model;
$model = clone $model_to_clone;
$model->id = null;
$model->deleted_at = null;
// Show the page
return view('models/edit')
->with('depreciation_list', Helper::depreciationList())
->with('item', $model)
->with('model_id', $model->id)
->with('cloned_model', $cloned_model);
->with('model_id', $model_to_clone->id)
->with('clone_model', $model_to_clone);
}
@@ -303,7 +321,7 @@ class AssetModelsController extends Controller
/**
* Returns a view that allows the user to bulk edit model attributes
* Returns a view that allows the user to bulk edit model attrbutes
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.7]

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\StorageHelper;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\AssetModel;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use \Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class AssetModelsFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @param UploadFileRequest $request
* @param int $modelId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, $modelId = null) : RedirectResponse
{
if (! $model = AssetModel::find($modelId)) {
return redirect()->route('models.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('update', $model);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/assetmodels')) {
Storage::makeDirectory('private_uploads/assetmodels', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$model->id,$file);
$model->logUpload($file_name, $request->get('notes'));
}
return redirect()->back()->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]
*/
public function show($modelId = null, $fileId = null) : StreamedResponse | Response | RedirectResponse | BinaryFileResponse
{
$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;
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]
*/
public function destroy($modelId = null, $fileId = null) : RedirectResponse
{
$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'));
}
}

View File

@@ -14,7 +14,6 @@ use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Validator;
class AssetCheckinController extends Controller
{
@@ -28,12 +27,18 @@ class AssetCheckinController extends Controller
* @param string $backto
* @since [v1.0]
*/
public function create(Asset $asset, $backto = null) : View | RedirectResponse
public function create($assetId, $backto = null) : View | RedirectResponse
{
// Check if the asset exists
if (is_null($asset = Asset::find($assetId))) {
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('checkin', $asset);
// This asset is already checked in, redirect
if (is_null($asset->assignedTo)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.already_checked_in'));
}
@@ -42,23 +47,7 @@ class AssetCheckinController extends Controller
return redirect()->route('hardware.show', $asset->id)->with('error', trans('admin/hardware/general.model_invalid_fix'));
}
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
if ($asset->isInvalid()) {
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
}
$target_option = match ($asset->assigned_type) {
'App\Models\Asset' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.asset_previous')]),
'App\Models\Location' => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.location')]),
default => trans('admin/hardware/form.redirect_to_type', ['type' => trans('general.user')]),
};
return view('hardware/checkin', compact('asset', 'target_option'))
->with('item', $asset)
->with('statusLabel_list', Helper::statusLabelList())
->with('backto', $backto)
->with('table_name', 'Assets');
return view('hardware/checkin', compact('asset'))->with('statusLabel_list', Helper::statusLabelList())->with('backto', $backto)->with('table_name', 'Assets');
}
/**
@@ -88,14 +77,12 @@ class AssetCheckinController extends Controller
$this->authorize('checkin', $asset);
session()->put('checkedInFrom', $asset->assignedTo->id);
session()->put('checkout_to_type', match ($asset->assigned_type) {
'App\Models\User' => 'user',
'App\Models\Location' => 'location',
'App\Models\Asset' => 'asset',
});
if ($asset->assignedType() == Asset::USER) {
$user = $asset->assignedTo;
}
$asset->expected_checkin = null;
$asset->last_checkin = now();
$asset->assignedTo()->disassociate($asset);
$asset->accepted = null;
$asset->name = $request->get('name');
@@ -104,9 +91,6 @@ class AssetCheckinController extends Controller
$asset->status_id = e($request->get('status_id'));
}
// Add any custom fields that should be included in the checkout
$asset->customFieldsForCheckinCheckout('display_checkin');
$this->migrateLegacyLocations($asset);
$asset->location_id = $asset->rtd_location_id;
@@ -122,14 +106,11 @@ class AssetCheckinController extends Controller
$originalValues = $asset->getRawOriginal();
// Handle last checkin date
$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');
}
$asset->last_checkin = $checkin_at;
$asset->licenseseats->each(function (LicenseSeat $seat) {
$seat->update(['assigned_to' => null]);
@@ -147,14 +128,10 @@ class AssetCheckinController extends Controller
session()->put('redirect_option', $request->get('redirect_option'));
// Add any custom fields that should be included in the checkout
$asset->customFieldsForCheckinCheckout('display_checkin');
if ($asset->save()) {
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues));
return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success', trans('admin/hardware/message.checkin.success'));
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))->with('success', trans('admin/hardware/message.checkin.success'));
}
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.error').$asset->getErrors());

View File

@@ -12,7 +12,6 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Session;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Validator;
class AssetCheckoutController extends Controller
{
@@ -27,33 +26,27 @@ class AssetCheckoutController extends Controller
* @since [v1.0]
* @return \Illuminate\Contracts\View\View
*/
public function create(Asset $asset) : View | RedirectResponse
public function create($assetId) : View | RedirectResponse
{
// Check if the asset exists
if (is_null($asset = Asset::with('company')->find(e($assetId)))) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('checkout', $asset);
if (!$asset->model) {
return redirect()->route('hardware.show', $asset)
->with('error', trans('admin/hardware/general.model_invalid_fix'));
return redirect()->route('hardware.show', $asset->id)->with('error', trans('admin/hardware/general.model_invalid_fix'));
}
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
if ($asset->isInvalid()) {
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
}
if ($asset->availableForCheckout()) {
return view('hardware/checkout', compact('asset'))
->with('statusLabel_list', Helper::deployableStatusLabelList())
->with('table_name', 'Assets')
->with('item', $asset);
->with('table_name', 'Assets');
}
return redirect()->route('hardware.index')
->with('error', trans('admin/hardware/message.checkout.not_available'));
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkout.not_available'));
}
/**
@@ -75,7 +68,7 @@ class AssetCheckoutController extends Controller
$this->authorize('checkout', $asset);
if (!$asset->model) {
return redirect()->route('hardware.show', $asset)->with('error', trans('admin/hardware/general.model_invalid_fix'));
return redirect()->route('hardware.show', $asset->id)->with('error', trans('admin/hardware/general.model_invalid_fix'));
}
$admin = auth()->user();
@@ -98,7 +91,6 @@ 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){
@@ -108,26 +100,23 @@ class AssetCheckoutController extends Controller
}
}
// Add any custom fields that should be included in the checkout
$asset->customFieldsForCheckinCheckout('display_checkout');
$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()->route('hardware.checkout.create', $asset)->with('error', trans('general.error_user_company'));
return redirect()->to("hardware/$assetId/checkout")->with('error', trans('general.error_user_company'));
}
}
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, $request->get('note'), $request->get('name'))) {
return Helper::getRedirectOption($request, $asset->id, 'Assets')
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
->with('success', trans('admin/hardware/message.checkout.success'));
}
// Redirect to the asset management page with error
return redirect()->route("hardware.checkout.create", $asset)->with('error', trans('admin/hardware/message.checkout.error').$asset->getErrors());
return redirect()->to("hardware/$assetId/checkout")->with('error', trans('admin/hardware/message.checkout.error').$asset->getErrors());
} catch (ModelNotFoundException $e) {
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($asset->getErrors());
} catch (CheckoutNotAllowed $e) {

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Http\Controllers\Assets;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use \Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class AssetFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @param UploadFileRequest $request
* @param int $assetId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, $assetId = null) : RedirectResponse
{
if (! $asset = Asset::find($assetId)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('update', $asset);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/assets')) {
Storage::makeDirectory('private_uploads/assets', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
$asset->logUpload($file_name, $request->get('notes'));
}
return redirect()->back()->with('success', trans('admin/hardware/message.upload.success'));
}
return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles'));
}
/**
* Check for permissions and display the file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @param int $fileId
* @since [v1.0]
*/
public function show($assetId = null, $fileId = null) : View | RedirectResponse | Response | StreamedResponse | BinaryFileResponse
{
if ($asset = Asset::find($assetId)) {
$this->authorize('view', $asset);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
$file = 'private_uploads/assets/'.$log->filename;
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('hardware.show', ['hardware' => $asset])->with('error', trans('general.file_not_found'));
}
}
return redirect()->route('hardware.show', ['hardware' => $asset])->with('error', trans('general.log_record_not_found'));
}
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
/**
* Delete the associated file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @param int $fileId
* @since [v1.0]
*/
public function destroy($assetId = null, $fileId = null) : RedirectResponse
{
$asset = Asset::find($assetId);
$this->authorize('update', $asset);
$rel_path = 'private_uploads/assets';
// the asset is valid
if (isset($asset->id)) {
$this->authorize('update', $asset);
$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'));
}
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
}

View File

@@ -6,7 +6,6 @@ use App\Events\CheckoutableCheckedIn;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\UpdateAssetRequest;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Support\Facades\Log;
@@ -31,7 +30,6 @@ use Illuminate\Http\Response;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use TypeError;
/**
* This class controls all actions related to assets for
@@ -149,7 +147,7 @@ class AssetsController extends Controller
$asset->byod = request('byod', 0);
if (! empty($settings->audit_interval)) {
$asset->next_audit_date = Carbon::now()->addMonths((int) $settings->audit_interval)->toDateString();
$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
@@ -157,16 +155,8 @@ class AssetsController extends Controller
$asset->location_id = $request->input('rtd_location_id', null);
}
if ($request->has('use_cloned_image')) {
$cloned_model_img = Asset::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'assets/'.$new_image_name;
Storage::disk('public')->copy('assets/'.$cloned_model_img->image, $new_image);
$asset->image = $new_image_name;
}
} else {
// Create the image (if one was chosen.)
if ($request->has('image')) {
$asset = $request->handleImages($asset);
}
@@ -196,31 +186,14 @@ class AssetsController extends Controller
// Validate the asset before saving
if ($asset->isValid() && $asset->save()) {
$target = null;
$location = null;
if ($userId = request('assigned_user')) {
$target = User::find($userId);
if (!$target) {
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.user'));
}
if (request('assigned_user')) {
$target = User::find(request('assigned_user'));
$location = $target->location_id;
} elseif ($assetId = request('assigned_asset')) {
$target = Asset::find($assetId);
if (!$target) {
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.asset'));
}
} elseif (request('assigned_asset')) {
$target = Asset::find(request('assigned_asset'));
$location = $target->location_id;
} elseif ($locationId = request('assigned_location')) {
$target = Location::find($locationId);
if (!$target) {
return redirect()->back()->withInput()->with('error', trans('admin/hardware/message.create.target_not_found.location'));
}
} elseif (request('assigned_location')) {
$target = Location::find(request('assigned_location'));
$location = $target->id;
}
@@ -228,38 +201,31 @@ class AssetsController extends Controller
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), $request->input('expected_checkin', null), 'Checked out on asset creation', $request->get('name'), $location);
}
$successes[] = "<a href='" . route('hardware.show', $asset) . "' style='color: white;'>" . e($asset->asset_tag) . "</a>";
$successes[] = "<a href='" . route('hardware.show', ['hardware' => $asset->id]) . "' style='color: white;'>" . e($asset->asset_tag) . "</a>";
} else {
$failures[] = join(",", $asset->getErrors()->all());
}
}
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
session()->put(['checkout_to_type' => $request->get('checkout_to_type'),
'other_redirect' => 'model' ]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($successes) {
if ($failures) {
//some succeeded, some failed
return Helper::getRedirectOption($request, $asset->id, 'Assets') //FIXME - not tested
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets')) //FIXME - not tested
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]))
->with('warning', trans_choice('admin/hardware/message.create.partial_failure', $failures, ['failures' => join("; ", $failures)]));
} else {
if (count($successes) == 1) {
//the most common case, keeping it so we don't have to make every use of that translation string be trans_choice'ed
//and re-translated
return Helper::getRedirectOption($request, $asset->id, 'Assets')
->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset), 'id', 'tag' => e($asset->asset_tag)]));
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', ['hardware' => $asset->id]), 'id', 'tag' => e($asset->asset_tag)]));
} else {
//multi-success
return Helper::getRedirectOption($request, $asset->id, 'Assets')
return redirect()->to(Helper::getRedirectOption($request, $asset->id, 'Assets'))
->with('success-unescaped', trans_choice('admin/hardware/message.create.multi_success_linked', $successes, ['links' => join(", ", $successes)]));
}
}
@@ -274,15 +240,20 @@ class AssetsController extends Controller
* Returns a view that presents a form to edit an existing asset.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return \Illuminate\Contracts\View\View
*/
public function edit(Asset $asset) : View | RedirectResponse
public function edit($assetId = null) : View | RedirectResponse
{
$this->authorize($asset);
session()->put('back_url', url()->previous());
return view('hardware/edit')
->with('item', $asset)
if (! $item = Asset::find($assetId)) {
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
//Handles company checks and permissions.
$this->authorize($item);
return view('hardware/edit', compact('item'))
->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList());
}
@@ -296,14 +267,15 @@ class AssetsController extends Controller
* @since [v1.0]
* @return \Illuminate\Contracts\View\View
*/
public function show(Asset $asset) : View | RedirectResponse
public function show($assetId = null) : View | RedirectResponse
{
$asset = Asset::withTrashed()->find($assetId);
$this->authorize('view', $asset);
$settings = Setting::getSettings();
if (isset($asset)) {
$audit_log = Actionlog::where('action_type', '=', 'audit')
->where('item_id', '=', $asset->id)
->where('item_id', '=', $assetId)
->where('item_type', '=', Asset::class)
->orderBy('created_at', 'DESC')->first();
@@ -319,7 +291,7 @@ class AssetsController extends Controller
$qr_code = (object) [
'display' => $settings->qr_code == '1',
'url' => route('qr_code/hardware', $asset),
'url' => route('qr_code/hardware', $asset->id),
];
return view('hardware/view', compact('asset', 'qr_code', 'settings'))
@@ -336,9 +308,14 @@ class AssetsController extends Controller
* @since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function update(ImageUploadRequest $request, Asset $asset) : RedirectResponse
public function update(ImageUploadRequest $request, $assetId = null) : RedirectResponse
{
// Check if the asset exists
if (! $asset = Asset::find($assetId)) {
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize($asset);
$asset->status_id = $request->input('status_id', null);
@@ -352,7 +329,7 @@ class AssetsController extends Controller
$asset->eol_explicit = false;
} elseif ($request->filled('asset_eol_date')) {
$asset->asset_eol_date = $request->input('asset_eol_date', null);
$months = (int) Carbon::parse($asset->asset_eol_date)->diffInMonths($asset->purchase_date, true);
$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;
@@ -374,16 +351,22 @@ class AssetsController extends Controller
$status = Statuslabel::find($request->input('status_id'));
// This is an archived or undeployable - we should check the asset back in.
// Pending is allowed here
if (($status) && (($status->getStatuslabelType() != 'pending') && ($status->getStatuslabelType() != 'deployable')) && ($target = $asset->assignedTo)) {
// This is a non-deployable status label - we should check the asset back in.
if (($status && $status->getStatuslabelType() != 'deployable') && ($target = $asset->assignedTo)) {
$originalValues = $asset->getRawOriginal();
$asset->assigned_to = null;
$asset->assigned_type = null;
$asset->accepted = null;
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on asset update with '.$status->getStatuslabelType().' status', date('Y-m-d H:i:s'), $originalValues));
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on asset update', date('Y-m-d H:i:s'), $originalValues));
}
if ($asset->assigned_to == '') {
$asset->location_id = $request->input('rtd_location_id', null);
}
if ($request->filled('image_delete')) {
try {
unlink(public_path().'/uploads/assets/'.$asset->image);
@@ -419,42 +402,35 @@ class AssetsController extends Controller
$asset = $request->handleImages($asset);
// Update custom fields in the database.
// Validation for these fields is handlded through the AssetRequest form request
// FIXME: No idea why this is returning a Builder error on db_column_name.
// Need to investigate and fix. Using static method for now.
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->element == 'checkbox' && !$request->has($field->db_column)) {
$asset->{$field->db_column} = null;
}
if ($request->has($field->db_column)) {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
}
}
}
session()->put([
'redirect_option' => $request->get('redirect_option'),
'checkout_to_type' => $request->get('checkout_to_type'),
'other_redirect' => $request->get('redirect_option') === 'other_redirect' ? 'model' : null,
]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($asset->save()) {
return Helper::getRedirectOption($request, $asset->id, 'Assets')
return redirect()->to(Helper::getRedirectOption($request, $assetId, 'Assets'))
->with('success', trans('admin/hardware/message.update.success'));
}
@@ -486,7 +462,7 @@ class AssetsController extends Controller
event(new CheckoutableCheckedIn($asset, $target, auth()->user(), 'Checkin on delete', $checkin_at, $originalValues));
DB::table('assets')
->where('id', $asset->id)
->update(['assigned_to' => null, 'assigned_type' => null]);
->update(['assigned_to' => null]);
}
@@ -498,7 +474,6 @@ class AssetsController extends Controller
}
}
$asset->delete();
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success'));
@@ -556,14 +531,14 @@ class AssetsController extends Controller
* @param int $assetId
* @since [v1.0]
*/
public function getQrCode(Asset $asset) : Response | BinaryFileResponse | string | bool
public function getQrCode($assetId = null) : Response | BinaryFileResponse | string | bool
{
$settings = Setting::getSettings();
if ($settings->label2_2d_type !== 'none') {
if ($settings->qr_code == '1') {
$asset = Asset::withTrashed()->find($assetId);
if ($asset) {
$size = Helper::barcodeDimensions($settings->label2_2d_type);
$size = Helper::barcodeDimensions($settings->barcode_type);
$qr_file = public_path().'/uploads/barcodes/qr-'.str_slug($asset->asset_tag).'-'.str_slug($asset->id).'.png';
if (isset($asset->id, $asset->asset_tag)) {
@@ -573,7 +548,7 @@ class AssetsController extends Controller
return response()->file($qr_file, $header);
} else {
$barcode = new \Com\Tecnick\Barcode\Barcode();
$barcode_obj = $barcode->getBarcodeObj($settings->label2_2d_type, route('hardware.show', $asset->id), $size['height'], $size['width'], 'black', [-2, -2, -2, -2]);
$barcode_obj = $barcode->getBarcodeObj($settings->barcode_type, route('hardware.show', $asset->id), $size['height'], $size['width'], 'black', [-2, -2, -2, -2]);
file_put_contents($qr_file, $barcode_obj->getPngData());
return response($barcode_obj->getPngData())->header('Content-type', 'image/png');
@@ -598,7 +573,7 @@ class AssetsController extends Controller
{
$settings = Setting::getSettings();
if ($asset = Asset::withTrashed()->find($assetId)) {
$barcode_file = public_path().'/uploads/barcodes/'.str_slug($settings->label2_1d_type).'-'.str_slug($asset->asset_tag).'.png';
$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)) {
@@ -611,11 +586,11 @@ class AssetsController extends Controller
$barcode = new \Com\Tecnick\Barcode\Barcode();
try {
$barcode_obj = $barcode->getBarcodeObj($settings->label2_1d_type, $asset->asset_tag, ($barcode_width < 300 ? $barcode_width : 300), 50);
$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|TypeError $e) {
} 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');
@@ -659,9 +634,8 @@ class AssetsController extends Controller
*/
public function getClone(Asset $asset)
{
$this->authorize('create', Asset::class);
$this->authorize('create', $asset);
$cloned = clone $asset;
$cloned_model = $asset;
$cloned->id = null;
$cloned->asset_tag = '';
$cloned->serial = '';
@@ -671,7 +645,6 @@ class AssetsController extends Controller
return view('hardware/edit')
->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList())
->with('cloned_model', $cloned_model)
->with('item', $cloned);
}
@@ -892,8 +865,8 @@ class AssetsController extends Controller
public function quickScan()
{
$this->authorize('audit', Asset::class);
$settings = Setting::getSettings();
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
$dt = Carbon::now()->addMonths(12)->toDateString();
return view('hardware/quickscan')->with('next_audit_date', $dt);
}
@@ -904,6 +877,15 @@ class AssetsController extends Controller
return view('hardware/quickscan-checkin')->with('statusLabel_list', Helper::statusLabelList());
}
public function audit($id)
{
$settings = Setting::getSettings();
$this->authorize('audit', Asset::class);
$dt = Carbon::now()->addMonths($settings->audit_interval)->toDateString();
$asset = Asset::findOrFail($id);
return view('hardware/audit')->with('asset', $asset)->with('next_audit_date', $dt)->with('locations_list');
}
public function dueForAudit()
{
@@ -920,73 +902,22 @@ class AssetsController extends Controller
}
public function audit(Asset $asset): View | RedirectResponse
public function auditStore(UploadFileRequest $request, $id)
{
$this->authorize('audit', Asset::class);
$settings = Setting::getSettings();
$rules = [
'location_id' => 'exists:locations,id|nullable|numeric',
'next_audit_date' => 'date|nullable',
];
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
$validator = Validator::make($request->all(), $rules);
if ($asset->isInvalid()) {
return redirect()->route('hardware.edit', $asset)->withErrors($asset->getErrors());
if ($validator->fails()) {
return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()->all()));
}
$dt = Carbon::now()->addMonths( (int) $settings->audit_interval)->toDateString();
return view('hardware/audit')->with('asset', $asset)->with('item', $asset)->with('next_audit_date', $dt)->with('locations_list');
}
public function auditStore(UploadFileRequest $request, Asset $asset)
{
$this->authorize('audit', Asset::class);
session()->put('redirect_option', $request->get('redirect_option'));
session()->put('other_redirect', 'audit');
$originalValues = $asset->getRawOriginal();
$asset->next_audit_date = $request->input('next_audit_date');
$asset->last_audit_date = date('Y-m-d H:i:s');
// 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') {
$asset->location_id = $request->input('location_id');
}
// Update custom fields in the database
if (($asset->model) && ($asset->model->fieldset)) {
foreach ($asset->model->fieldset->fields as $field) {
if (($field->display_audit=='1') && ($request->has($field->db_column))) {
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
}
}
}
}
// Invoke the validation to see if the audit will complete successfully
$asset->setRules($asset->getRules() + $asset->customFieldValidationRules());
// Validate the rest of the data before we turn off the event dispatcher
if ($asset->isInvalid()) {
return redirect()->back()->withInput()->withErrors($asset->getErrors());
}
$asset = Asset::findOrFail($id);
/**
* Even though we do a save() further down, we don't want to log this as a "normal" asset update,
@@ -1002,11 +933,18 @@ class AssetsController extends Controller
* which manually invokes Watson Validating to make sure the asset's model is valid.
*
* @see \App\Observers\AssetObserver::updating()
* @see \App\Models\Asset::save()
*/
$asset->unsetEventDispatcher();
$asset->next_audit_date = $request->input('next_audit_date');
$asset->last_audit_date = date('Y-m-d H:i:s');
// 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') {
$asset->location_id = $request->input('location_id');
}
/**
* Invoke Watson Validating to check the asset itself and check to make sure it saved correctly.
@@ -1020,8 +958,8 @@ class AssetsController extends Controller
$file_name = $request->handleFile('private_uploads/audits/', 'audit-'.$asset->id, $request->file('image'));
}
$asset->logAudit($request->input('note'), $request->input('location_id'), $file_name, $originalValues);
return Helper::getRedirectOption($request, $asset->id, 'Assets')->with('success', trans('admin/hardware/message.audit.success'));
$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()->back()->withInput()->withErrors($asset->getErrors());

View File

@@ -52,26 +52,11 @@ class BulkAssetsController extends Controller
}
$asset_ids = $request->input('ids');
if ($request->input('bulk_actions') === 'checkout') {
$status_check =$this->hasUndeployableStatus($asset_ids);
if($status_check && $status_check['status'] === true){
$asset_tags = implode(', ', array_column($status_check['tags'], 'asset_tag'));
$asset_ids = $status_check['asset_ids'];
session()->flash('warning', trans('admin/hardware/message.undeployable', ['asset_tags' => $asset_tags]));
}
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect()->route('hardware.bulkcheckout.show');
}
if ($request->input('bulk_actions') === 'maintenance') {
$request->session()->flashInput(['selected_assets' => $asset_ids]);
return redirect()->route('maintenances.create');
}
// 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]);
@@ -112,47 +97,11 @@ class BulkAssetsController extends Controller
// This handles all of the pivot sorting below (versus the assets.* fields in the allowed_columns array)
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.id';
$query = Asset::with('assignedTo', 'location', 'model')
$assets = Asset::with('assignedTo', 'location', 'model')
->whereIn('assets.id', $asset_ids)
->withTrashed();
switch ($sort_override) {
case 'model':
$query->OrderModels($order);
break;
case 'model_number':
$query->OrderModelNumber($order);
break;
case 'category':
$query->OrderCategory($order);
break;
case 'manufacturer':
$query->OrderManufacturer($order);
break;
case 'company':
$query->OrderCompany($order);
break;
case 'location':
$query->OrderLocation($order);
break;
case 'rtd_location':
$query->OrderRtdLocation($order);
break;
case 'status_label':
$query->OrderStatus($order);
break;
case 'supplier':
$query->OrderSupplier($order);
break;
case 'assigned_to':
$query->OrderAssigned($order);
break;
default:
$query->orderBy($column_sort, $order);
break;
}
$assets = $query->get();
$assets = $assets->get();
if ($assets->isEmpty()) {
Log::debug('No assets were found for the provided IDs', ['ids' => $asset_ids]);
@@ -161,7 +110,6 @@ class BulkAssetsController extends Controller
$models = $assets->unique('model_id');
$modelNames = [];
foreach($models as $model) {
$modelNames[] = $model->model->name;
}
@@ -197,6 +145,7 @@ class BulkAssetsController extends Controller
case 'edit':
$this->authorize('update', Asset::class);
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList())
@@ -205,7 +154,40 @@ class BulkAssetsController extends Controller
}
}
switch ($sort_override) {
case 'model':
$assets->OrderModels($order);
break;
case 'model_number':
$assets->OrderModelNumber($order);
break;
case 'category':
$assets->OrderCategory($order);
break;
case 'manufacturer':
$assets->OrderManufacturer($order);
break;
case 'company':
$assets->OrderCompany($order);
break;
case 'location':
$assets->OrderLocation($order);
case 'rtd_location':
$assets->OrderRtdLocation($order);
break;
case 'status_label':
$assets->OrderStatus($order);
break;
case 'supplier':
$assets->OrderSupplier($order);
break;
case 'assigned_to':
$assets->OrderAssigned($order);
break;
default:
$assets->orderBy($column_sort, $order);
break;
}
return redirect()->back()->with('error', 'No action selected');
}
@@ -224,25 +206,13 @@ class BulkAssetsController extends Controller
$error_array = array();
// Get the back url from the session and then destroy the session
$bulk_back_url = route('hardware.index');
$bulk_back_url = $request->session()->pull('bulk_back_url', url()->previous());
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
// find custom field input attributes that start with 'null_'
$null_custom_fields_inputs = array_filter($request->all(), function ($key) {
// filter out all keys that start with 'null_'
return (strpos($key, 'null_') === 0);
}, ARRAY_FILTER_USE_KEY);;
// remove 'null' from the keys
$custom_fields_to_null = [];
foreach ($null_custom_fields_inputs as $key => $value) {
$custom_fields_to_null[str_replace('null', '', $key)] = $value;
if ($request->session()->has('bulk_back_url')) {
$bulk_back_url = $request->session()->pull('bulk_back_url');
}
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
if (! $request->filled('ids') || count($request->input('ids')) == 0) {
@@ -281,9 +251,7 @@ class BulkAssetsController extends Controller
|| ($request->filled('null_expected_checkin_date'))
|| ($request->filled('null_next_audit_date'))
|| ($request->filled('null_asset_eol_date'))
|| ($request->filled('null_notes'))
|| ($request->anyFilled($custom_field_columns))
|| ($request->anyFilled(array_keys($null_custom_fields_inputs)))
) {
// Let's loop through those assets and build an update array
@@ -306,14 +274,10 @@ class BulkAssetsController extends Controller
->conditionallyAddItem('supplier_id')
->conditionallyAddItem('warranty_months')
->conditionallyAddItem('next_audit_date')
->conditionallyAddItem('asset_eol_date')
->conditionallyAddItem('notes');
->conditionallyAddItem('asset_eol_date');
foreach ($custom_field_columns as $key => $custom_field_column) {
$this->conditionallyAddItem($custom_field_column);
}
foreach ($custom_fields_to_null as $key => $custom_field_to_null) {
$this->conditionallyAddItem($key);
}
if (!($asset->eol_explicit)) {
if ($request->filled('model_id')) {
@@ -364,10 +328,6 @@ class BulkAssetsController extends Controller
}
}
if ($request->input('null_notes')=='1') {
$this->update_array['notes'] = null;
}
if ($request->filled('purchase_cost')) {
@@ -398,22 +358,16 @@ class BulkAssetsController extends Controller
* to someone/something.
*/
if ($request->filled('status_id')) {
try {
$updated_status = Statuslabel::findOrFail($request->input('status_id'));
} catch (ModelNotFoundException $e) {
return redirect($bulk_back_url)->with('error', trans('admin/statuslabels/message.does_not_exist'));
}
$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.
$unassigned = $asset->assigned_to == '';
$deployable = $updated_status->deployable == '1' && $asset->assetstatus?->deployable == '1';
$pending = $updated_status->pending === 1;
if ($unassigned || $deployable || $pending) {
if (
($asset->assigned_to == '')
|| ($updated_status->deployable == '1') && ($asset->assetstatus->deployable == '1')
) {
$this->update_array['status_id'] = $updated_status->id;
}
@@ -465,7 +419,6 @@ class BulkAssetsController extends Controller
}
/**
*
* Start all the custom fields shenanigans
*/
@@ -473,15 +426,6 @@ class BulkAssetsController extends Controller
if ($asset->model->fieldset) {
foreach ($asset->model->fieldset->fields as $field) {
// null custom fields
if ($custom_fields_to_null) {
foreach ($custom_fields_to_null as $key => $custom_field_to_null) {
if ($field->db_column == $key) {
$this->update_array[$field->db_column] = null;
}
}
}
if ((array_key_exists($field->db_column, $this->update_array)) && ($field->field_encrypted == '1')) {
if (Gate::allows('admin')) {
$decrypted_old = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
@@ -540,13 +484,7 @@ class BulkAssetsController extends Controller
} // end asset foreach
if ($has_errors > 0) {
session()->put('bulkedit_ids', $request->input('ids'));
session()->put('bulk_asset_errors',$error_array);
return redirect()
->route('hardware.index')
->with('bulk_asset_errors', $error_array)
->withInput();
return redirect($bulk_back_url)->with('bulk_asset_errors', $error_array);
}
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success'));
@@ -587,31 +525,21 @@ 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');
}
$assetIds = $request->get('ids');
if(empty($assetIds)) {
return redirect($bulk_back_url)->with('error', trans('admin/hardware/message.delete.nothing_updated'));
}
$assignedAssets = Asset::whereIn('id', $assetIds)->whereNotNull('assigned_to')->get();
if($assignedAssets->isNotEmpty()) {
//if assets are checked out, return a list of asset tags that would need to be checked in first.
$assetTags = $assignedAssets->pluck('asset_tag')->implode(', ');
return redirect($bulk_back_url)->with('error', trans_choice('admin/hardware/message.delete.assigned_to_error', $assignedAssets->count(), ['asset_tag' => $assetTags] ));
}
foreach (Asset::wherein('id', $assetIds)->get() as $asset) {
if ($request->filled('ids')) {
$assets = Asset::find($request->get('ids'));
foreach ($assets as $asset) {
$asset->delete();
}
} // endforeach
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.delete.success'));
return redirect($bulk_back_url)->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'));
}
/**
@@ -620,10 +548,7 @@ class BulkAssetsController extends Controller
public function showCheckout() : View
{
$this->authorize('checkout', Asset::class);
$do_not_change = ['' => trans('general.do_not_change')];
$status_label_list = $do_not_change + Helper::deployableStatusLabelList();
return view('hardware/bulk-checkout')->with('statusLabel_list', $status_label_list);
return view('hardware/bulk-checkout');
}
/**
@@ -631,6 +556,7 @@ class BulkAssetsController extends Controller
*/
public function storeCheckout(AssetCheckoutRequest $request) : RedirectResponse | ModelNotFoundException
{
$this->authorize('checkout', Asset::class);
try {
@@ -644,8 +570,6 @@ class BulkAssetsController extends Controller
$asset_ids = array_filter($request->get('selected_assets'));
$assets = Asset::findOrFail($asset_ids);
if (request('checkout_to_type') == 'asset') {
foreach ($asset_ids as $asset_id) {
if ($target->id == $asset_id) {
@@ -655,25 +579,21 @@ class BulkAssetsController extends Controller
}
$checkout_at = date('Y-m-d H:i:s');
if (($request->filled('checkout_at')) && ($request->get('checkout_at') != date('Y-m-d'))) {
$checkout_at = $request->get('checkout_at');
$checkout_at = e($request->get('checkout_at'));
}
$expected_checkin = '';
if ($request->filled('expected_checkin')) {
$expected_checkin = $request->get('expected_checkin');
$expected_checkin = e($request->get('expected_checkin'));
}
$errors = [];
DB::transaction(function () use ($target, $admin, $checkout_at, $expected_checkin, &$errors, $assets, $request) { //NOTE: $errors is passsed by reference!
foreach ($assets as $asset) {
DB::transaction(function () use ($target, $admin, $checkout_at, $expected_checkin, &$errors, $asset_ids, $request) { //NOTE: $errors is passsed by reference!
foreach ($asset_ids as $asset_id) {
$asset = Asset::findOrFail($asset_id);
$this->authorize('checkout', $asset);
// See if there is a status label passed
if ($request->filled('status_id')) {
$asset->status_id = $request->get('status_id');
}
$checkout_success = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), $asset->name, null);
//TODO - I think this logic is duplicated in the checkOut method?
@@ -698,7 +618,7 @@ class BulkAssetsController extends Controller
// Redirect to the asset management page with error
return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans_choice('admin/hardware/message.multi-checkout.error', $asset_ids))->withErrors($errors);
} catch (ModelNotFoundException $e) {
return redirect()->route('hardware.bulkcheckout.show')->withInput()->with('error', trans_choice('admin/hardware/message.multi-checkout.error', $request->input('selected_assets')));
return redirect()->route('hardware.bulkcheckout.show')->with('error', $e->getErrors());
}
}
@@ -717,54 +637,4 @@ class BulkAssetsController extends Controller
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success'));
}
}
public function hasUndeployableStatus (array $asset_ids)
{
$undeployable = Asset::whereIn('id', $asset_ids)
->undeployable()
->get();
$undeployableTags = $undeployable->map(function ($asset) {
return [
'id' => $asset->id,
'asset_tag' => $asset->asset_tag,
];
})->toArray();
$undeployableIds = array_column($undeployableTags, 'id');
$filtered_ids = array_diff($asset_ids, $undeployableIds);
if($undeployable->isNotEmpty()) {
return ['status' => true, 'tags' => $undeployableTags, 'asset_ids' => $filtered_ids];
}
return false;
}
public function bulkEditForm(): View|RedirectResponse
{
$this->authorize('update', Asset::class);
$asset_ids = session()->pull('bulkedit_ids', []);
if (empty($asset_ids)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.update.no_assets_selected'));
}
$assets = Asset::with('model')->withTrashed()->whereIn('id', $asset_ids)->get();
if ($assets->isEmpty()) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.update.assets_do_not_exist_or_are_invalid'));
}
$models = $assets->unique('model_id');
$modelNames = [];
foreach ($models as $model) {
$modelNames[] = $model->model->name;
}
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList())
->with('models', $models->pluck(['model']))
->with('modelNames', $modelNames);
}
}

View File

@@ -50,14 +50,14 @@ class ForgotPasswordController extends Controller
*/
public function sendResetLinkEmail(Request $request)
{
/**
* Let's set a max character count here to prevent potential
* buffer overflow issues with attackers sending very large
* payloads through. The addition of the string rule prevents attackers
* sending arrays through and causing 500s
* payloads through.
*/
$request->validate([
'username' => ['required', 'max:255', 'string'],
'username' => ['required', 'max:255'],
]);
/**

View File

@@ -206,7 +206,6 @@ class LoginController extends Controller
$user->password = bcrypt($request->input('password'));
}
$user->last_login = \Carbon::now();
$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.
@@ -227,7 +226,7 @@ class LoginController extends Controller
$strip_prefixes = [
// IIS/AD
// https://github.com/grokability/snipe-it/pull/5862
// https://github.com/snipe/snipe-it/pull/5862
'\\',
// Google Cloud IAP
@@ -284,11 +283,8 @@ class LoginController extends Controller
return redirect()->back()->withInput()->withErrors($validator);
}
// Set the custom lockout attempts from the env and sett the custom lockout throttle from the env.
// We divide decayMinutes by 60 here to get minutes, since Laravel changed the default from minutes
// to seconds, and we don't want to break limits on existing systems
$this->maxAttempts = config('auth.passwords.users.throttle.max_attempts');
$this->decayMinutes = (config('auth.passwords.users.throttle.lockout_duration') / 60);
$this->maxLoginAttempts = config('auth.passwords.users.throttle.max_attempts');
$this->lockoutTime = config('auth.passwords.users.throttle.lockout_duration');
if ($lockedOut = $this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
@@ -358,7 +354,7 @@ class LoginController extends Controller
// We wouldn't normally see this page if 2FA isn't enforced via the
// \App\Http\Middleware\CheckForTwoFactor middleware AND if a device isn't enrolled,
// but let's check anyway in case there's a browser history or back button thing.
// but let's check check anyway in case there's a browser history or back button thing.
// While you can access this page directly, enrolling a device when 2FA isn't enforced
// won't cause any harm.
@@ -436,7 +432,6 @@ class LoginController extends Controller
if (Google2FA::verifyKey($user->two_factor_secret, $secret)) {
$user->two_factor_enrolled = 1;
$user->last_login = \Carbon::now();
$user->saveQuietly();
$request->session()->put('2fa_authed', $user->id);
@@ -484,7 +479,6 @@ class LoginController extends Controller
}
$request->session()->regenerate(true);
$request->session()->forget('2fa_authed');
if ($request->session()->has('password_hash_'.Auth::getDefaultDriver())){
$request->session()->remove('password_hash_'.Auth::getDefaultDriver());
@@ -525,6 +519,45 @@ class LoginController extends Controller
return 'username';
}
/**
* Redirect the user after determining they are locked out.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendLockoutResponse(Request $request)
{
$seconds = $this->limiter()->availableIn(
$this->throttleKey($request)
);
$minutes = round($seconds / 60);
$message = trans('auth/message.throttle', ['minutes' => $minutes]);
return redirect()->back()
->withInput($request->only($this->username(), 'remember'))
->withErrors([$this->username() => $message]);
}
/**
* Override the lockout time and duration
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function hasTooManyLoginAttempts(Request $request)
{
$lockoutTime = config('auth.passwords.users.throttle.lockout_duration');
$maxLoginAttempts = config('auth.passwords.users.throttle.max_attempts');
return $this->limiter()->tooManyAttempts(
$this->throttleKey($request),
$maxLoginAttempts,
$lockoutTime
);
}
public function legacyAuthRedirect()
{

View File

@@ -103,24 +103,22 @@ class ResetPasswordController extends Controller
], $messages);
}
if ($user->ldap_import != '1') {
// set the response
$response = $broker->reset(
$this->credentials($request), function ($user, $password) {
$this->resetPassword($user, $password);
});
// 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 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'));
}

View File

@@ -71,28 +71,20 @@ class BulkAssetModelsController extends Controller
if (($request->filled('manufacturer_id') && ($request->input('manufacturer_id') != 'NC'))) {
$update_array['manufacturer_id'] = $request->input('manufacturer_id');
}
if (($request->filled('category_id') && ($request->input('category_id') != 'NC'))) {
$update_array['category_id'] = $request->input('category_id');
}
if ($request->input('fieldset_id') != 'NC') {
$update_array['fieldset_id'] = $request->input('fieldset_id');
}
if ($request->input('depreciation_id') != 'NC') {
$update_array['depreciation_id'] = $request->input('depreciation_id');
}
if ($request->input('requestable') != '') {
if ($request->filled('requestable') != '') {
$update_array['requestable'] = $request->input('requestable');
}
if ($request->filled('min_amt')) {
$update_array['min_amt'] = $request->input('min_amt');
}
if (count($update_array) > 0) {
AssetModel::whereIn('id', $models_raw_array)->update($update_array);

View File

@@ -68,9 +68,7 @@ class CategoriesController extends Controller
$category->eula_text = $request->input('eula_text');
$category->use_default_eula = $request->input('use_default_eula', '0');
$category->require_acceptance = $request->input('require_acceptance', '0');
$category->alert_on_response = $request->input('alert_on_response', '0');
$category->checkin_email = $request->input('checkin_email', '0');
$category->notes = $request->input('notes');
$category->created_by = auth()->id();
$category = $request->handleImages($category);
@@ -89,10 +87,14 @@ class CategoriesController extends Controller
* @param int $categoryId
* @since [v1.0]
*/
public function edit(Category $category) : RedirectResponse | View
public function edit($categoryId = null) : RedirectResponse | View
{
$this->authorize('update', Category::class);
return view('categories/edit')->with('item', $category)
if (is_null($item = Category::find($categoryId))) {
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.does_not_exist'));
}
return view('categories/edit', compact('item'))
->with('category_types', Helper::categoryTypeList());
}
@@ -105,10 +107,19 @@ class CategoriesController extends Controller
* @param int $categoryId
* @since [v1.0]
*/
public function update(ImageUploadRequest $request, Category $category) : RedirectResponse
public function update(ImageUploadRequest $request, $categoryId = null) : RedirectResponse
{
$this->authorize('update', Category::class);
if (is_null($category = Category::find($categoryId))) {
// Redirect to the categories management page
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.does_not_exist'));
}
// Update the category data
$category->name = $request->input('name');
// If the item count is > 0, we disable the category type in the edit. Disabled items
// don't POST, so if the category_type is blank we just set it to the default.
// Don't allow the user to change the category_type once it's been created
if (($request->filled('category_type') && ($category->itemCount() > 0))) {
@@ -122,9 +133,7 @@ class CategoriesController extends Controller
$category->eula_text = $request->input('eula_text');
$category->use_default_eula = $request->input('use_default_eula', '0');
$category->require_acceptance = $request->input('require_acceptance', '0');
$category->alert_on_response = $request->input('alert_on_response', '0');
$category->checkin_email = $request->input('checkin_email', '0');
$category->notes = $request->input('notes');
$category = $request->handleImages($category);
@@ -147,7 +156,7 @@ class CategoriesController extends Controller
{
$this->authorize('delete', Category::class);
// Check if the category exists
if (is_null($category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count')->findOrFail($categoryId))) {
if (is_null($category = Category::findOrFail($categoryId))) {
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.not_found'));
}
@@ -157,6 +166,7 @@ class CategoriesController extends Controller
Storage::disk('public')->delete('categories'.'/'.$category->image);
$category->delete();
// Redirect to the locations management page
return redirect()->route('categories.index')->with('success', trans('admin/categories/message.delete.success'));
}
@@ -169,10 +179,10 @@ class CategoriesController extends Controller
* @param $id
* @since [v1.8]
*/
public function show(Category $category) : View | RedirectResponse
public function show($id) : View | RedirectResponse
{
$this->authorize('view', Category::class);
if ($category = Category::find($id)) {
if ($category->category_type == 'asset') {
$category_type = 'hardware';
$category_type_route = 'assets';
@@ -187,5 +197,8 @@ class CategoriesController extends Controller
return view('categories/view', compact('category'))
->with('category_type', $category_type)
->with('category_type_route', $category_type_route);
}
return redirect()->route('categories.index')->with('error', trans('admin/categories/message.does_not_exist'));
}
}

View File

@@ -60,7 +60,6 @@ final class CompaniesController extends Controller
$company->phone = $request->input('phone');
$company->fax = $request->input('fax');
$company->email = $request->input('email');
$company->notes = $request->input('notes');
$company->created_by = auth()->id();
$company = $request->handleImages($company);
@@ -80,10 +79,16 @@ final class CompaniesController extends Controller
* @since [v1.8]
* @param int $companyId
*/
public function edit(Company $company) : View | RedirectResponse
public function edit($companyId) : View | RedirectResponse
{
$this->authorize('update', $company);
return view('companies/edit')->with('item', $company);
if (is_null($item = Company::find($companyId))) {
return redirect()->route('companies.index')
->with('error', trans('admin/companies/message.does_not_exist'));
}
$this->authorize('update', $item);
return view('companies/edit')->with('item', $item);
}
/**
@@ -94,15 +99,18 @@ final class CompaniesController extends Controller
* @param ImageUploadRequest $request
* @param int $companyId
*/
public function update(ImageUploadRequest $request, Company $company) : RedirectResponse
public function update(ImageUploadRequest $request, $companyId) : RedirectResponse
{
if (is_null($company = Company::find($companyId))) {
return redirect()->route('companies.index')->with('error', trans('admin/companies/message.does_not_exist'));
}
$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->notes = $request->input('notes');
$company = $request->handleImages($company);
@@ -123,13 +131,11 @@ final class CompaniesController extends Controller
*/
public function destroy($companyId) : RedirectResponse
{
if (is_null($company = Company::find($companyId))) {
return redirect()->route('companies.index')
->with('error', trans('admin/companies/message.not_found'));
}
$this->authorize('delete', $company);
if (! $company->isDeletable()) {
return redirect()->route('companies.index')
@@ -150,9 +156,15 @@ final class CompaniesController extends Controller
->with('success', trans('admin/companies/message.delete.success'));
}
public function show(Company $company) : View | RedirectResponse
public function show($id) : View | RedirectResponse
{
$this->authorize('view', Company::class);
if (is_null($company = Company::find($id))) {
return redirect()->route('companies.index')
->with('error', trans('admin/companies/message.not_found'));
}
return view('companies/view')->with('company', $company);
}
}

View File

@@ -100,8 +100,8 @@ class ComponentCheckinController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.checkin.success'));
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success',
trans('admin/components/message.checkin.success'));
}
return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));

View File

@@ -120,7 +120,6 @@ class ComponentCheckoutController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.checkout.success'));
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.checkout.success'));
}
}

View File

@@ -88,16 +88,10 @@ class ComponentsController extends Controller
$component = $request->handleImages($component);
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($component->save()) {
return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.create.success'));
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($component->getErrors());
@@ -113,14 +107,15 @@ class ComponentsController extends Controller
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function edit(Component $component)
public function edit($componentId = null)
{
if ($item = Component::find($componentId)) {
$this->authorize('update', $item);
$this->authorize('update', $component);
session()->put('back_url', url()->previous());
return view('components/edit')
->with('item', $component)
->with('category_type', 'component');
return view('components/edit', compact('item'))->with('category_type', 'component');
}
return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));
}
@@ -135,8 +130,11 @@ class ComponentsController extends Controller
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v3.0]
*/
public function update(ImageUploadRequest $request, Component $component)
public function update(ImageUploadRequest $request, $componentId = null)
{
if (is_null($component = Component::find($componentId))) {
return redirect()->route('components.index')->with('error', trans('admin/components/message.does_not_exist'));
}
$min = $component->numCheckedOut();
$validator = Validator::make($request->all(), [
'qty' => "required|numeric|min:$min",
@@ -171,8 +169,7 @@ class ComponentsController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($component->save()) {
return Helper::getRedirectOption($request, $component->id, 'Components')
->with('success', trans('admin/components/message.update.success'));
return redirect()->to(Helper::getRedirectOption($request, $component->id, 'Components'))->with('success', trans('admin/components/message.update.success'));
}
return redirect()->back()->withInput()->withErrors($component->getErrors());
@@ -204,10 +201,6 @@ class ComponentsController extends Controller
}
}
if ($component->numCheckedOut() > 0) {
return redirect()->route('components.index')->with('error', trans('admin/components/message.delete.error_qty'));
}
$component->delete();
return redirect()->route('components.index')->with('success', trans('admin/components/message.delete.success'));
@@ -223,9 +216,17 @@ class ComponentsController extends Controller
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show(Component $component)
public function show($componentId = null)
{
$component = Component::find($componentId);
if (isset($component->id)) {
$this->authorize('view', $component);
return view('components/view', compact('component'));
}
// Redirect to the user management page
return redirect()->route('components.index')
->with('error', trans('admin/components/message.does_not_exist'));
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace App\Http\Controllers\Components;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Component;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\JsonResponse;
use Illuminate\Support\Facades\Log;
class ComponentsFilesController extends Controller
{
/**
* Validates and stores files associated with a component.
*
* @param UploadFileRequest $request
* @param int $componentId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(UploadFileRequest $request, $componentId = null)
{
if (config('app.lock_passwords')) {
return redirect()->route('components.show', ['component'=>$componentId])->with('error', trans('general.feature_disabled'));
}
$component = Component::find($componentId);
if (isset($component->id)) {
$this->authorize('update', $component);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/components')) {
Storage::makeDirectory('private_uploads/components', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/components/','component-'.$component->id, $file);
//Log the upload to the log
$component->logUpload($file_name, e($request->input('notes')));
}
return redirect()->route('components.show', $component->id)->with('success', trans('general.file_upload_success'));
}
return redirect()->route('components.show', $component->id)->with('error', trans('general.no_files_uploaded'));
}
// Prepare the error message
return redirect()->route('components.index')
->with('error', trans('general.file_does_not_exist'));
}
/**
* Deletes the selected component file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $componentId
* @param int $fileId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($componentId = null, $fileId = null)
{
$component = Component::find($componentId);
// the asset is valid
if (isset($component->id)) {
$this->authorize('update', $component);
$log = Actionlog::find($fileId);
// Remove the file if one exists
if (Storage::exists('components/'.$log->filename)) {
try {
Storage::delete('components/'.$log->filename);
} catch (\Exception $e) {
Log::debug($e);
}
}
$log->delete();
return redirect()->back()
->with('success', trans('admin/hardware/message.deletefile.success'));
}
// Redirect to the licence management page
return redirect()->route('components.index')->with('error', trans('general.file_does_not_exist'));
}
/**
* Allows the selected file to be viewed.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.4]
* @param int $componentId
* @param int $fileId
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show($componentId = null, $fileId = null)
{
Log::debug('Private filesystem is: '.config('filesystems.default'));
// the component is valid
if ($component = Component::find($componentId)) {
$this->authorize('view', $component);
$this->authorize('components.files', $component);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $component->id)->find($fileId)) {
$file = 'private_uploads/components/'.$log->filename;
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('components.show', ['component' => $component])->with('error', trans('general.file_not_found'));
}
}
return redirect()->route('components.show', ['component' => $component])->with('error', trans('general.log_record_not_found'));
}
return redirect()->route('components.index')->with('error', trans('general.file_does_not_exist', ['id' => $fileId]));
}
}

View File

@@ -111,7 +111,6 @@ class ConsumableCheckoutController extends Controller
// Redirect to the new consumable page
return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
->with('success', trans('admin/consumables/message.checkout.success'));
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.checkout.success'));
}
}

View File

@@ -7,7 +7,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Company;
use App\Models\Consumable;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
@@ -50,7 +50,7 @@ class ConsumablesController extends Controller
{
$this->authorize('create', Consumable::class);
return view('consumables.edit')->with('category_type', 'consumable')
return view('consumables/edit')->with('category_type', 'consumable')
->with('item', new Consumable);
}
@@ -81,33 +81,16 @@ class ConsumablesController extends Controller
$consumable->purchase_date = $request->input('purchase_date');
$consumable->purchase_cost = $request->input('purchase_cost');
$consumable->qty = $request->input('qty');
$consumable->created_by = auth()->id();
$consumable->created_by = auth()->id();
$consumable->notes = $request->input('notes');
if ($request->has('use_cloned_image')) {
$cloned_model_img = Consumable::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'consumables/'.$new_image_name;
Storage::disk('public')->copy('consumables/'.$cloned_model_img->image, $new_image);
$consumable->image = $new_image_name;
}
} else {
$consumable = $request->handleImages($consumable);
}
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
$consumable = $request->handleImages($consumable);
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($consumable->save()) {
return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
->with('success', trans('admin/consumables/message.create.success'));
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.create.success'));
}
return redirect()->back()->withInput()->withErrors($consumable->getErrors());
@@ -121,14 +104,15 @@ class ConsumablesController extends Controller
* @see ConsumablesController::postEdit() method that stores the form data.
* @since [v1.0]
*/
public function edit(Consumable $consumable) : View | RedirectResponse
public function edit($consumableId = null) : View | RedirectResponse
{
$this->authorize($consumable);
session()->put('back_url', url()->previous());
return view('consumables/edit')
->with('item', $consumable)
->with('category_type', 'consumable');
if ($item = Consumable::find($consumableId)) {
$this->authorize($item);
return view('consumables/edit', compact('item'))->with('category_type', 'consumable');
}
return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.does_not_exist'));
}
/**
@@ -142,8 +126,11 @@ class ConsumablesController extends Controller
* @see ConsumablesController::getEdit() method that stores the form data.
* @since [v1.0]
*/
public function update(StoreConsumableRequest $request, Consumable $consumable)
public function update(StoreConsumableRequest $request, $consumableId = null)
{
if (is_null($consumable = Consumable::find($consumableId))) {
return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.does_not_exist'));
}
$min = $consumable->numCheckedOut();
$validator = Validator::make($request->all(), [
@@ -178,8 +165,7 @@ class ConsumablesController extends Controller
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($consumable->save()) {
return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
->with('success', trans('admin/consumables/message.update.success'));
return redirect()->to(Helper::getRedirectOption($request, $consumable->id, 'Consumables'))->with('success', trans('admin/consumables/message.update.success'));
}
return redirect()->back()->withInput()->withErrors($consumable->getErrors());
@@ -216,11 +202,16 @@ class ConsumablesController extends Controller
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show(Consumable $consumable)
public function show($consumableId = null)
{
$consumable = Consumable::withCount('users as users_consumables')->find($consumable->id);
$consumable = Consumable::withCount('users as users_consumables')->find($consumableId);
$this->authorize($consumable);
return view('consumables/view', compact('consumable'));
if (isset($consumable->id)) {
return view('consumables/view', compact('consumable'));
}
return redirect()->route('consumables.index')
->with('error', trans('admin/consumables/message.does_not_exist'));
}
public function clone(Consumable $consumable) : View
@@ -229,10 +220,9 @@ class ConsumablesController extends Controller
$consumable_to_close = $consumable;
$consumable = clone $consumable_to_close;
$consumable->id = null;
$consumable->image = null;
$consumable->created_by = null;
return view('consumables/edit')
->with('cloned_model', $consumable_to_close)
->with('item', $consumable);
return view('consumables/edit')->with('item', $consumable);
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\Http\Controllers\Consumables;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Consumable;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use Symfony\Consumable\HttpFoundation\JsonResponse;
use Illuminate\Support\Facades\Log;
class ConsumablesFilesController extends Controller
{
/**
* Validates and stores files associated with a consumable.
*
* @param UploadFileRequest $request
* @param int $consumableId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @todo Switch to using the AssetFileRequest form request validator.
*/
public function store(UploadFileRequest $request, $consumableId = null)
{
if (config('app.lock_passwords')) {
return redirect()->route('consumables.show', ['consumable'=>$consumableId])->with('error', trans('general.feature_disabled'));
}
$consumable = Consumable::find($consumableId);
if (isset($consumable->id)) {
$this->authorize('update', $consumable);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/consumables')) {
Storage::makeDirectory('private_uploads/consumables', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/consumables/','consumable-'.$consumable->id, $file);
//Log the upload to the log
$consumable->logUpload($file_name, e($request->input('notes')));
}
return redirect()->route('consumables.show', $consumable->id)->with('success', trans('general.file_upload_success'));
}
return redirect()->route('consumables.show', $consumable->id)->with('error', trans('general.no_files_uploaded'));
}
// Prepare the error message
return redirect()->route('consumables.index')
->with('error', trans('general.file_does_not_exist'));
}
/**
* Deletes the selected consumable file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $consumableId
* @param int $fileId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($consumableId = null, $fileId = null)
{
$consumable = Consumable::find($consumableId);
// the asset is valid
if (isset($consumable->id)) {
$this->authorize('update', $consumable);
$log = Actionlog::find($fileId);
// Remove the file if one exists
if (Storage::exists('consumables/'.$log->filename)) {
try {
Storage::delete('consumables/'.$log->filename);
} catch (\Exception $e) {
Log::debug($e);
}
}
$log->delete();
return redirect()->back()
->with('success', trans('admin/hardware/message.deletefile.success'));
}
// Redirect to the licence management page
return redirect()->route('consumables.index')->with('error', trans('general.file_does_not_exist'));
}
/**
* Allows the selected file to be viewed.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.4]
* @param int $consumableId
* @param int $fileId
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show($consumableId = null, $fileId = null)
{
$consumable = Consumable::find($consumableId);
// the consumable is valid
if (isset($consumable->id)) {
$this->authorize('view', $consumable);
$this->authorize('consumables.files', $consumable);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $consumable->id)->find($fileId)) {
$file = 'private_uploads/consumables/'.$log->filename;
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('consumables.show', ['consumable' => $consumable])->with('error', trans('general.file_not_found'));
}
}
// The log record doesn't exist somehow
return redirect()->route('consumables.show', ['consumable' => $consumable])->with('error', trans('general.log_record_not_found'));
}
return redirect()->route('consumables.index')->with('error', trans('general.file_does_not_exist', ['id' => $fileId]));
}
}

View File

@@ -22,15 +22,6 @@
namespace App\Http\Controllers;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Location;
use App\Models\Maintenance;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
@@ -41,45 +32,6 @@ abstract class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
static $map_object_type = [
'accessories' => Accessory::class,
'maintenances' => Maintenance::class,
'assets' => Asset::class,
'components' => Component::class,
'consumables' => Consumable::class,
'hardware' => Asset::class,
'licenses' => License::class,
'locations' => Location::class,
'models' => AssetModel::class,
'users' => User::class,
];
static $map_storage_path = [
'accessories' => 'private_uploads/accessories/',
'maintenances' => 'private_uploads/maintenances/',
'assets' => 'private_uploads/assets/',
'components' => 'private_uploads/components/',
'consumables' => 'private_uploads/consumables/',
'hardware' => 'private_uploads/assets/',
'licenses' => 'private_uploads/licenses/',
'locations' => 'private_uploads/locations/',
'models' => 'private_uploads/models/',
'users' => 'private_uploads/users/',
];
static $map_file_prefix= [
'accessories' => 'accessory',
'maintenances' => 'maintenance',
'assets' => 'asset',
'components' => 'component',
'consumables' => 'consumable',
'hardware' => 'asset',
'licenses' => 'license',
'locations' => 'location',
'models' => 'model',
'users' => 'user',
];
public function __construct()
{
view()->share('signedIn', Auth::check());

Some files were not shown because too many files have changed in this diff Show More