Compare commits

..

1347 Commits

Author SHA1 Message Date
snipe
dffff07436 Removed codacy config 2026-03-13 18:29:21 +00:00
snipe
f69e2c671b Merge pull request #18709 from grokability/pint-resources
Applied pint to languages and resources
2026-03-13 18:22:55 +00:00
snipe
84fdb5d6c1 Applied pint to languages and resources 2026-03-13 18:17:51 +00:00
snipe
dc58ddce59 Merge pull request #18708 from grokability/pint-database
Apply pint to database directory
2026-03-13 18:14:02 +00:00
snipe
b5a46a370f Apply pint to database directory 2026-03-13 18:12:37 +00:00
snipe
199bdb219f Merge pull request #18707 from grokability/pint-config
Apply pint to config directory
2026-03-13 18:11:02 +00:00
snipe
c1a93e3ac8 Apply pint to config directory 2026-03-13 18:08:12 +00:00
snipe
f334b8caa3 Merge pull request #18706 from grokability/pint-providers
Apply pint to Providers directory
2026-03-13 18:03:57 +00:00
snipe
8b658a19b9 Apply pint to Providers directory 2026-03-13 17:56:51 +00:00
snipe
59dd9970d0 Merge pull request #18705 from grokability/pint-presenters
Apply pint to Presenters directory
2026-03-13 17:53:04 +00:00
snipe
55d46cbefe Apply pint to Presenters directory 2026-03-13 17:43:54 +00:00
snipe
e37ce27773 Merge pull request #18704 from grokability/pint-policies-and-observers
Apply pint to Observers and Policy directories
2026-03-13 17:42:57 +00:00
snipe
b2c0a21230 Apply pint to Observers and Policy directories 2026-03-13 17:41:08 +00:00
snipe
00704aea73 Merge pull request #18703 from grokability/pint-notifications
Apply pint to Notifications directory
2026-03-13 17:39:24 +00:00
snipe
31043d1f5c Apply pint to Notifications directory 2026-03-13 17:37:47 +00:00
snipe
33b68c11db Merge pull request #18702 from grokability/pint-mail
Apply pint to Mail directory
2026-03-13 17:32:33 +00:00
snipe
de607e7d83 Apply pint to Mail directory 2026-03-13 17:26:56 +00:00
snipe
ab7ff30a73 Merge pull request #18701 from grokability/pint-livewire
Applied pint to Livewire directory
2026-03-13 17:25:21 +00:00
snipe
53f2ef2ca1 Applied pint to Livewire directory 2026-03-13 17:19:19 +00:00
snipe
25f2e560f1 Merge pull request #18700 from grokability/pint-jobs-listeners
Apply pint to Jobs and Listeners directory
2026-03-13 17:18:21 +00:00
snipe
317b1a462e Apply pint to Jobs and Listeners directory 2026-03-13 17:13:08 +00:00
snipe
bf97734533 Merge pull request #18699 from grokability/pint-importer
Apply pint to Importer directory
2026-03-13 17:11:35 +00:00
snipe
3e831bf9b3 Apply pint to Importer directory 2026-03-13 17:10:06 +00:00
snipe
b4400b38a9 Merge pull request #18698 from grokability/pint-traits-and-transformers
Applied pint to Traits and Transformers
2026-03-13 17:07:54 +00:00
snipe
a613380811 Applied pint to Traits and Transformers 2026-03-13 17:02:28 +00:00
snipe
7f30fe1a95 Merge pull request #18697 from grokability/pint-requests
Apply pint to Requests directory
2026-03-13 17:00:47 +00:00
snipe
93168326da Apply pint to Requests directory 2026-03-13 16:56:26 +00:00
snipe
70c672eb52 Merge pull request #18696 from grokability/pint-middleware
Apply pint to Middleware directory
2026-03-13 16:54:17 +00:00
snipe
ec6caf9b59 Apply pint to Middleware directory 2026-03-13 16:53:11 +00:00
snipe
1f1d41ecfd Merge pull request #18695 from grokability/pint-controllers
Apply pint to non-API controllers
2026-03-13 16:50:28 +00:00
snipe
9bc92f57c8 Apply pint to non-API controllers 2026-03-13 16:45:30 +00:00
snipe
bbe6475eb2 Merge pull request #18694 from grokability/pint-api-controllers
Apply pint to API controllers
2026-03-13 16:43:33 +00:00
snipe
1e5d426e70 Apply pint to API controllers 2026-03-13 16:38:23 +00:00
snipe
ccf6856143 Apply pint to Helpers 2026-03-13 16:37:19 +00:00
snipe
175d8306b8 Merge pull request #18693 from grokability/pint-exceptions
Apply pint to Exceptions directory
2026-03-13 16:35:43 +00:00
snipe
2e7046a810 Apply pint to Exceptions directory 2026-03-13 16:32:11 +00:00
snipe
c03c913ce7 Merge pull request #18692 from grokability/pint-enums
Apply pint to enums and events
2026-03-13 16:30:52 +00:00
snipe
f7b82ad1ff Apply pint to enums and events 2026-03-13 16:25:59 +00:00
snipe
118ddfce94 Merge pull request #18691 from grokability/pint-console
Apply pint to Console directory
2026-03-13 16:24:39 +00:00
snipe
8bce38b918 Apply pint to Console directory 2026-03-13 16:20:24 +00:00
snipe
d31ba20dd5 Merge pull request #18690 from grokability/pint-actions
Use pint on actions directory
2026-03-13 16:19:11 +00:00
snipe
a3c7410c35 Apply pint to actions directory 2026-03-13 16:04:34 +00:00
snipe
624e6839c3 Merge pull request #18689 from grokability/pint-models
Use pint on models directory
2026-03-13 16:02:51 +00:00
snipe
9623fa4d87 Use pint on models directory 2026-03-13 15:55:28 +00:00
snipe
c196637922 Install pint dev dependency 2026-03-13 14:52:13 +00:00
snipe
67284e2e6d Updated badge in readme 2026-03-13 11:17:36 +00:00
snipe
f6ee500e6c Merge pull request #18684 from marcusmoore/copilot-updates
Updated copilot instructions
2026-03-13 11:16:41 +00:00
snipe
b76d909619 Merge pull request #18685 from marcusmoore/test-improvements
Improved asset tests
2026-03-13 11:16:29 +00:00
snipe
faf9cecc10 Merge pull request #18688 from grokability/normalize-requests-with-class-reference
Use Blah::class instead of new Blah in form requests
2026-03-13 11:09:42 +00:00
snipe
1246de2644 Use Blah::class instead of new Blah in form requests 2026-03-13 11:02:05 +00:00
snipe
0c72109ad8 Revert RMB for more consistent UX 2026-03-13 09:34:26 +00:00
Marcus Moore
5aad15256c Improve tests around assets 2026-03-12 16:15:27 -07:00
Marcus Moore
69f7778067 Revise copilot instructions 2026-03-12 15:56:37 -07:00
snipe
5d6b9890ca Import eloquent model 2026-03-12 18:44:45 +00:00
snipe
0d257d956f Undo json encode 2026-03-12 14:54:18 +00:00
snipe
4c05f26940 Merge pull request #18681 from grokability/redirect-fixes
Use intended() for redirect options
2026-03-12 14:44:38 +00:00
snipe
ce18ff669c Added admin check 2026-03-12 14:42:36 +00:00
snipe
a3b5346773 Ignore the sqlite database in git 2026-03-12 14:24:59 +00:00
snipe
3c96491295 Remove unused route 2026-03-12 14:24:46 +00:00
snipe
53abf8cdcc Load count with RMB controller 2026-03-12 14:10:43 +00:00
snipe
e376492128 Use intended() for redirect options 2026-03-12 13:57:50 +00:00
snipe
2658b9b064 Fixed variable in trans_choice for deployable check 2026-03-12 13:47:23 +00:00
snipe
f412b56caa Fixed #18678 - small UI tweaks 2026-03-12 13:40:47 +00:00
snipe
9c63b40a5a Merge pull request #18680 from grokability/copilot
Added copilot instructions file
2026-03-12 13:21:38 +00:00
snipe
40843f93dc Added copilot instructions file 2026-03-12 13:20:37 +00:00
snipe
9165f59dcc Normalized requiered colors 2026-03-12 12:23:20 +00:00
snipe
92ff333778 Force foreground color for label form-control 2026-03-12 12:13:59 +00:00
snipe
63e62cde1b Moved gates higher, switch to RMB for accessories 2026-03-12 12:13:38 +00:00
snipe
c5081ce3e5 Added colors to setting seeder 2026-03-12 11:47:04 +00:00
snipe
67d2a5d094 Use better maintenance name 2026-03-12 11:44:40 +00:00
snipe
a170da5c01 Merge pull request #18679 from grokability/normalize-breadcrumb-text
Normalize breadcrumb text
2026-03-12 11:42:27 +00:00
snipe
9652cb312a Normalize breadcrumb text 2026-03-12 11:34:38 +00:00
snipe
e4d7e08902 Merge pull request #18672 from marcusmoore/remove-laravel-collective-dep
Fixed #17199: Remove Laravel Collective HTML dependency
2026-03-11 19:53:25 +00:00
snipe
428095b71b Fixed #18670 - set nav link color override in ResetDemo console command 2026-03-11 17:56:58 +00:00
Marcus Moore
411ffb12ca Merge branch 'develop' into remove-laravel-collective-dep 2026-03-11 10:41:06 -07:00
snipe
3b93193da1 Merge pull request #18667 from marcusmoore/migrate-link-methods
Fixed #18666: Migrate Laravel Collective helper methods
2026-03-11 10:14:31 +00:00
snipe
224a813f25 Fixed #18668 - changed button type 2026-03-11 09:33:03 +00:00
Marcus Moore
7d079f74a1 Uninstall laravelcollective/html 2026-03-10 16:52:42 -07:00
Marcus Moore
4ca53e6f70 Remove Collective provider and aliases 2026-03-10 16:45:48 -07:00
Marcus Moore
70d1ffe294 Remove MacroServiceProvider and macros.php 2026-03-10 16:39:30 -07:00
Marcus Moore
335e1a7e18 Merge branch 'develop' into migrate-link-methods 2026-03-10 16:35:23 -07:00
Marcus Moore
287481a44e Fix model reference 2026-03-10 13:38:14 -07:00
Marcus Moore
2dd09f9702 Remove unused method 2026-03-10 13:35:25 -07:00
snipe
9317d6551d Fixed #18653 - “select company” to “company” in user edit 🙄 2026-03-10 20:25:53 +00:00
snipe
ed3d30e343 Merge pull request #18657 from marcusmoore/form-macros
Fixed #17200 and #17201: Remove alt_barcode_types and barcode_types macros
2026-03-10 20:11:01 +00:00
Marcus Moore
ca5a25f703 Replace remaining calls to link_to_route in Presenters 2026-03-10 13:06:46 -07:00
Marcus Moore
692a9ebebf Replace call to link_to_route 2026-03-10 11:58:27 -07:00
Marcus Moore
bf314a0f84 Replace call to link_to_route 2026-03-10 11:54:29 -07:00
Marcus Moore
c8c2bb6709 Remove unused serialUrl method from LicensePresenter 2026-03-10 11:31:07 -07:00
Marcus Moore
3fc8b976fc Merge branch 'develop' into form-macros
# Conflicts:
#	resources/macros/macros.php
2026-03-10 10:12:31 -07:00
snipe
6b9dc97fa1 Merge pull request #18665 from grokability/#18662-fix-seat-search
Fixed #18662 wire up search box in assigned license seats
2026-03-10 16:10:37 +00:00
snipe
4f3a30261e Fixed #18661 - return true/false in JSON 2026-03-10 15:42:35 +00:00
snipe
e7c478318c Fixed donked route 2026-03-10 15:39:29 +00:00
snipe
e75860c6ee Fixed #18662 wire up search box in assigned license seats 2026-03-10 15:31:54 +00:00
snipe
d0dbd1e561 Link parent company 2026-03-10 11:40:03 +00:00
snipe
e7eb4f0e80 More display_name 2026-03-10 10:25:01 +00:00
snipe
676a995889 Use update check for files controller api 2026-03-10 10:22:39 +00:00
snipe
a44fe14de1 Use display_name in more places 2026-03-10 10:14:27 +00:00
snipe
0015dbcd1d Merge pull request #18656 from marcusmoore/form-macro-username-select
Fixed #17208: Replace username_format macro
2026-03-10 09:42:15 +00:00
snipe
fc5e7cccbc Merge pull request #18652 from grokability/dependabot/github_actions/develop/docker/login-action-4
Bump docker/login-action from 3 to 4
2026-03-10 09:41:54 +00:00
dependabot[bot]
8987f3f951 Bump docker/login-action from 3 to 4
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 09:30:14 +00:00
snipe
ba671a8f1f Merge pull request #18651 from grokability/dependabot/github_actions/develop/docker/build-push-action-7
Bump docker/build-push-action from 6 to 7
2026-03-10 09:29:32 +00:00
snipe
fbe871f8d1 Merge pull request #18650 from grokability/dependabot/github_actions/develop/docker/metadata-action-6
Bump docker/metadata-action from 5 to 6
2026-03-10 09:29:10 +00:00
snipe
116b2d1229 Merge pull request #18649 from grokability/dependabot/github_actions/develop/docker/setup-buildx-action-4
Bump docker/setup-buildx-action from 3 to 4
2026-03-10 09:28:03 +00:00
snipe
20fd870b59 Merge pull request #18658 from marcusmoore/form-macro-countries
Fixed #17202: Replaced countries form macro
2026-03-10 09:27:05 +00:00
snipe
5c46990195 Check for user on enable_sounds 2026-03-10 09:20:35 +00:00
snipe
fab57020f2 Prevent browser errors since input field is display none 2026-03-10 09:16:24 +00:00
snipe
b37074f473 Null check on status (RB-4087) 2026-03-10 09:03:57 +00:00
snipe
24e2e81a28 Use component table in suppliers 2026-03-10 09:00:29 +00:00
Marcus Moore
ccabc1fbcc Remove countries macro 2026-03-09 17:37:58 -07:00
Marcus Moore
f847f83cb8 Replace macro in location modal 2026-03-09 17:36:26 -07:00
Marcus Moore
e0771827aa Replace country in address partial 2026-03-09 17:34:57 -07:00
Marcus Moore
fa6adaa155 Migrate countries macro on user page 2026-03-09 17:22:43 -07:00
Marcus Moore
f3504ce6fc Remove barcode_types macro 2026-03-09 16:36:54 -07:00
Marcus Moore
a075ca904b Remove alt_barcode_types macro 2026-03-09 16:36:26 -07:00
Marcus Moore
e3fb6fabf8 Remove username_format maco 2026-03-09 16:30:12 -07:00
Marcus Moore
6f89af790e Migrate username format to blade component 2026-03-09 16:29:55 -07:00
snipe
28f493d84d Escape pivot notes 2026-03-09 09:06:58 +00:00
dependabot[bot]
a87e862148 Bump docker/build-push-action from 6 to 7
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:42:34 +00:00
dependabot[bot]
6e264bfee0 Bump docker/metadata-action from 5 to 6
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:42:27 +00:00
dependabot[bot]
1ecf862f2d Bump docker/setup-buildx-action from 3 to 4
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 08:42:22 +00:00
snipe
338fc88095 Merge pull request #18647 from grokability/revert-18601-chore/security-upgrade-passport13-socialite-jwt7
Revert "Upgrade Passport to v13 and move php-jwt to v7 to remediate JWT advisory"
2026-03-08 12:32:31 +00:00
snipe
0a724cc49a Revert "Upgrade Passport to v13 and move php-jwt to v7 to remediate JWT advisory" 2026-03-08 12:29:56 +00:00
snipe
a2efcf1ca9 Merge pull request #18601 from ubc-cpsc/chore/security-upgrade-passport13-socialite-jwt7
Upgrade Passport to v13 and move php-jwt to v7 to remediate JWT advisory
2026-03-08 11:51:15 +00:00
snipe
c9d73e85e1 Fixed RB-4083 2026-03-08 11:37:19 +00:00
Joël Pittet
a956676745 Move Passport 13 key permission normalization to upgrade.php to cover existing installs with incorrect permissions 2026-03-07 22:22:46 -08:00
Joël Pittet
5800d08202 normalize key permissions 2026-03-07 21:59:07 -08:00
Joël Pittet
b8958bad72 Test failure fixes from 'Key Files Permissions Validation' from https://github.com/laravel/passport/blob/13.x/UPGRADE.md#key-files-permissions-validation 2026-03-07 21:59:00 -08:00
Joël Pittet
0508d7558e OAuth Client Table Changes (Optional) from https://github.com/laravel/passport/blob/13.x/UPGRADE.md#oauth-client-table-changes-optional 2026-03-07 21:59:00 -08:00
Joël Pittet
98c3192a13 Bump laravel/socialite to latest patch release, upgrade laravel/passport to 13 which is compatible with Laravel 11, and security release for firebase/php-jwt 2026-03-07 21:59:00 -08:00
snipe
b886e11670 Fixed user API tests 2026-03-07 20:50:18 +00:00
snipe
92bb1df82e Removed error log in test 2026-03-07 11:05:33 +00:00
snipe
1c4549dd8e Added word break on history 2026-03-07 11:04:25 +00:00
snipe
83e61ec8cd Unset admin if auth user is not admin 2026-03-07 10:45:57 +00:00
snipe
7ea549df4a Added .journal to gitignore 2026-03-06 21:57:23 +00:00
snipe
af1c55cd7e Small code re-org for clarity 2026-03-06 21:55:46 +00:00
snipe
7822c0bc3e Merge pull request #18644 from uberbrady/fix_location_observer
Re-add Location Observer with null-safe companyable check
2026-03-06 21:47:58 +00:00
Brady Wetherington
2b7ed1f7fa Re-add Location Observer with null-safe companyable check 2026-03-06 21:43:19 +00:00
snipe
45ddaf1b72 Temp comment out location observer 2026-03-06 20:14:56 +00:00
snipe
7595c922fa Fixed #18043 - added location logging 2026-03-06 14:03:37 +00:00
snipe
4d4513d936 Fixed #18642 - view-assets mobile 2026-03-06 13:40:01 +00:00
snipe
e378c99923 Layout tweakas 2026-03-06 13:29:38 +00:00
snipe
078c870970 Merge pull request #18162 from Godmartinz/status_bulk_select_fix
Adds #17685 better warning for bulk status change to undeployable type
2026-03-06 09:58:41 +01:00
snipe
4a91d37423 Merge pull request #17666 from akemidx/xxdays-since-last-update
Feature: Added xx days since last update to Custom Report
2026-03-06 09:54:32 +01:00
snipe
96befb2aef Merge branch 'develop' into xxdays-since-last-update 2026-03-06 09:54:01 +01:00
snipe
925452e106 Merge pull request #18248 from iryadifarhan/fix/depreciation-report-displays-incorrect-monthly-depreciation-value
Fixes inaccurate monthly depreciation value displayed at depreciation report when using a floor value
2026-03-06 09:40:39 +01:00
snipe
1678330f0c Merge pull request #18426 from akemidx/global-report-templates
Fixed: Added global report templates
2026-03-06 09:30:15 +01:00
snipe
58cf3fc2ea Dev assets 2026-03-06 08:17:48 +00:00
snipe
13ce17ffc6 Merge pull request #18494 from marcusmoore/livewire4
Upgraded Livewire to v4
2026-03-06 09:16:25 +01:00
snipe
70e020ee0c Merge pull request #18637 from marcusmoore/test-updates
Fixed some minor issues in tests
2026-03-06 09:16:00 +01:00
snipe
c6a167ef40 Removed min-height (this might go badly) 2026-03-06 07:42:28 +00:00
snipe
420ba5f261 Fixed #18341 - remove action button from user profile view 2026-03-06 06:45:46 +00:00
snipe
7aa1f3ae1d Merge pull request #18639 from grokability/#18282-add-user-notes-to-custom-export
Fixed #18282 - added original notes to checkout target in custom report
2026-03-06 07:23:58 +01:00
snipe
556d638136 Fixed #18282 - added original notes to checkout target in custom report 2026-03-06 06:19:16 +00:00
snipe
c4aa10baf8 Added user company to custom asset report 2026-03-06 05:59:11 +00:00
snipe
86f647e714 Improved bulk audit bg 2026-03-06 05:39:09 +00:00
snipe
64f5d40fd9 Removed duplicate code 2026-03-06 05:21:59 +00:00
snipe
1a0869c2ca Merge pull request #18628 from marcusmoore/fixes/fd-54108-checkin-all-button
Fixed Checkin All Seats button
2026-03-06 06:17:12 +01:00
snipe
1fb32ee461 Merge pull request #18638 from grokability/experiments/compact-nav
Compacted nav UI, components for buttons
2026-03-06 05:45:02 +01:00
snipe
68a863e63e Updated test, fixed route 2026-03-06 04:38:48 +00:00
snipe
a1e62ccd46 Fixed (shimmed) #18636 - added shim roite for fieldsets 2026-03-06 03:54:33 +00:00
snipe
dae66c0fa4 Fixed audit background 2026-03-06 03:48:45 +00:00
snipe
1d6b21e1c7 Switched to newer button model 2026-03-06 03:33:04 +00:00
snipe
455be12058 Gate fix for admin 2026-03-06 03:05:40 +00:00
snipe
2ebb3ecb96 Update views with new button blades 2026-03-06 01:17:00 +00:00
Marcus Moore
b29056dddf Delete duplicate test 2026-03-05 16:35:15 -08:00
Marcus Moore
3ed09f304c Fix method name 2026-03-05 16:30:05 -08:00
Marcus Moore
4875283ed1 Fix test route 2026-03-05 16:20:27 -08:00
Marcus Moore
261231c09e Fix method and variable names 2026-03-05 15:55:59 -08:00
snipe
180dad6ee6 Use updated infobox 2026-03-05 14:36:12 +00:00
snipe
d7121ad82f Removed class active (this is handled in jquery) 2026-03-05 11:46:55 +00:00
snipe
f121c61524 Added hidden-print class to action buttons column 2026-03-05 11:45:21 +00:00
Marcus Moore
93bf91869b Fix checkin all seats button 2026-03-03 14:57:43 -08:00
snipe
2ed32612b9 Fixed typo in comments 2026-03-02 18:35:57 +01:00
snipe
314bcbe208 Added assets upload url singleton 2026-03-02 18:35:57 +01:00
snipe
6290e34623 Exclude show route for settings API [RB-17718] 2026-03-02 18:35:57 +01:00
snipe
5e5521a128 Merge pull request #18623 from grokability/dependabot/github_actions/develop/actions/upload-artifact-7
Bump actions/upload-artifact from 6 to 7
2026-03-02 12:51:02 +00:00
dependabot[bot]
1e8bed86d3 Bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 08:43:13 +00:00
snipe
8bc7d50e35 Updated to check gate on object 2026-02-26 16:03:57 +00:00
Marcus Moore
aa420e5006 Bump Livewire to v4.1.4 2026-02-25 13:30:30 -08:00
Marcus Moore
4babaa85fc Merge branch 'develop' into livewire4
# Conflicts:
#	composer.lock
2026-02-25 13:29:07 -08:00
snipe
378d9e008e Removed truncate that ran after maintenance seeder 2026-02-25 19:46:09 +00:00
snipe
340fabe4f6 Merge pull request #18614 from spencerrlongg/eof-rollbar
Fixes (hopefully) RB #19772 Unexpected EOF
2026-02-25 19:05:30 +00:00
spencerrlongg
29331b8b06 missing semicolon - kind of a shot in the dark 2026-02-25 12:54:43 -06:00
snipe
6fbcaa0959 Make sure manager_id is an integer 2026-02-25 18:41:53 +00:00
snipe
52e10205c5 Add license ID to tabke cookie 2026-02-25 18:40:29 +00:00
snipe
f419ae3f4a Fixed license history target 2026-02-25 17:41:26 +00:00
snipe
84a6b8e012 Add display_name to asset advanced search filter 2026-02-25 16:54:54 +00:00
snipe
f234aee691 Use display_name in transformUserCompact() 2026-02-25 16:41:30 +00:00
snipe
623d516595 Merge pull request #18095 from marcusmoore/5947-bulk-checkout-rollup
#5947 - roll up bulk asset checkout email
2026-02-25 15:36:58 +00:00
snipe
156eaf53d5 Fix for #18408 2026-02-25 15:30:37 +00:00
snipe
678a7c1437 Removed BS class 2026-02-25 15:04:24 +00:00
snipe
ce2a760093 Merge pull request #17964 from Godmartinz/add-License-checkedout-to-user
Adds #11741 currently assigned license table to license checkout
2026-02-25 14:50:39 +00:00
snipe
34b02d8534 Merge pull request #18485 from marcusmoore/debugbar
Bumped Debugbar to v4
2026-02-25 14:43:03 +00:00
snipe
42e0e691dd Merge pull request #18609 from ubc-cpsc/bugfix/php-deprecated-string-interpolation
Fix deprecated string interpolation in controllers
2026-02-25 14:41:24 +00:00
snipe
ccfda99060 Merge pull request #18613 from grokability/add-sorting-on-model-name-and-number-for-maintenances
Added model number as a separate field, added sorting
2026-02-25 14:22:48 +00:00
snipe
57f02bdc57 Fixed typo 2026-02-25 14:22:27 +00:00
snipe
7056656eab Added model number as a separate field, added sorting 2026-02-25 14:19:01 +00:00
snipe
2d899fc772 Merge pull request #18612 from grokability/added-maintenances-seeder
Added maintenances seeder
2026-02-25 14:16:44 +00:00
snipe
80ee0835aa Added maintenances seeder 2026-02-25 14:12:13 +00:00
snipe
7b7e6c7971 Fixed maintence image display 2026-02-25 12:39:29 +00:00
snipe
025fb30335 Added user email to exportable field in custom report 2026-02-25 12:03:23 +00:00
snipe
fcace4a192 Override the table search with the asset tag search if one is passed 2026-02-25 11:50:52 +00:00
snipe
619e1c70ea Fixed #18603 - set unique cookie ID on asset history table 2026-02-25 11:48:42 +00:00
Joël Pittet
7a95bdfba6 Fix deprecated string interpolation in controllers 2026-02-24 14:57:29 -08:00
Marcus Moore
89962de161 Move to Traits directory 2026-02-24 11:37:07 -08:00
Marcus Moore
d9328bd0ce Merge branch 'develop' into debugbar 2026-02-24 11:27:04 -08:00
snipe
81306df06a Merge pull request #18606 from grokability/#18600-check-filesystem-on-healthcheck
Fixed #18600 - add filesystem check on health checker
2026-02-24 14:00:14 +00:00
snipe
75abe9eb3e Re-add try/catch for DB for cleaner output 2026-02-24 13:23:12 +00:00
snipe
1f26bf1125 Fixed #18600 - add filesystem check on health checker 2026-02-24 13:17:15 +00:00
snipe
14b9c06d77 Remove optional qualifier 2026-02-24 12:00:06 +00:00
snipe
4f3d23d1ef Merge pull request #18576 from marcusmoore/fixes/rb-20713-license-seat-assigned-to
Fixed RB-20713: Improved validation of license seat update api endpoint
2026-02-24 07:29:41 +00:00
Marcus Moore
cada43f34d Allow checking in from unfound targets 2026-02-23 17:23:42 -08:00
snipe
9188f89a2a Tagged v8.4.0 2026-02-23 20:40:40 +00:00
Marcus Moore
1cc7ef0543 Organization 2026-02-23 10:51:34 -08:00
snipe
a86ffc29d1 Added display name to search term in advanced search 2026-02-23 14:43:55 +00:00
snipe
9ed5540c49 Merge pull request #18597 from uberbrady/improve_ldap_binding
Improve LDAP escaping to better reflect modern PHP standards
2026-02-23 13:38:28 +00:00
Brady Wetherington
3a7e00ccc3 Improve LDAP escaping to better reflect modern PHP standards 2026-02-23 13:01:00 +00:00
snipe
f82cdabccd More info anel refinements 2026-02-23 11:41:45 +00:00
snipe
b9c9cc0046 Merge pull request #18596 from grokability/add-with-trashed-to-formattednamelink
Adds withTrashed() to `adminuser` and updates presenter to show strikethrough
2026-02-23 11:00:43 +00:00
snipe
27ece84d52 Adds withTrashed() to adminuser and updates presenter to show strikethrough 2026-02-23 10:49:44 +00:00
snipe
1299186fb8 Merge pull request #18591 from grokability/#18017-show-more-info-in-bulk-audit
Fixed #18017 - show status on bulk audit
2026-02-21 13:17:31 +00:00
snipe
d837e4845b Show note 2026-02-21 13:16:58 +00:00
snipe
39be0c5590 Fixed #18017 - show status on bulk audit 2026-02-21 13:08:23 +00:00
snipe
d2bb10e96d Merge pull request #18590 from grokability/optionally-update-audit-dates
Added checkbox to determine if existing audit dates should be changed
2026-02-21 12:53:12 +00:00
snipe
f139a616c7 Added checkbox to determine if existing audit dates should be changed 2026-02-21 12:49:36 +00:00
snipe
d5099d973b Move copy icon to right side 2026-02-20 15:00:04 +00:00
snipe
82b18a3207 Updated requestable icon 2026-02-20 14:15:50 +00:00
snipe
10aee6bb5f Fixed tab pane name 2026-02-20 13:10:14 +00:00
snipe
c1b11ab9bf Small fixes for accessories/checkedout tab 2026-02-20 13:05:31 +00:00
snipe
c226a061f5 Tagged pre-release 2026-02-20 12:42:09 +00:00
snipe
c0be738aef Removed unneeded return check 2026-02-20 12:13:01 +00:00
snipe
e6987ec148 Early return on non-existent acceptance 2026-02-20 12:11:49 +00:00
snipe
973fa7a58b Merge pull request #18575 from spencerrlongg/encrypted-custom-fields-api
Adds field_encrypted To Fields API Response
2026-02-20 12:11:00 +00:00
snipe
b6195ba3ae Default active tab fix 2026-02-20 11:56:23 +00:00
snipe
d3bd213f29 Fixed typo in tooltip text 2026-02-20 09:49:06 +00:00
snipe
9502dd8bd7 Back out :class 2026-02-20 09:36:38 +00:00
Marcus Moore
e81ccf4280 Add more failing assertions 2026-02-19 16:53:22 -08:00
Marcus Moore
9730f2c0f3 Add some failing assertions 2026-02-19 16:40:45 -08:00
Marcus Moore
1de34c0656 Some controller clean up 2026-02-19 16:40:34 -08:00
Marcus Moore
bdce6b1387 Add test case 2026-02-19 13:51:20 -08:00
Marcus Moore
ce0c2ecd8f Populate new tests 2026-02-19 13:47:52 -08:00
Marcus Moore
40f07e3d72 Scaffold addition test cases 2026-02-19 11:50:04 -08:00
snipe
3c3ccae7c9 Allow context overwrites in FCO specific tables 2026-02-19 19:14:26 +00:00
Godfrey M
be211f6f86 fix apostophes 2026-02-19 09:57:29 -08:00
snipe
9789ca42f8 Highlight inventory in red if below min 2026-02-19 15:11:27 +00:00
snipe
da26bc5165 Updated JS assets 2026-02-19 14:55:45 +00:00
snipe
0b67626961 Merge pull request #18580 from grokability/fixes-for-alert-menu
Quick fix for alert menu 1001 queries
2026-02-19 14:54:50 +00:00
snipe
546c8ce7d5 Quick fix for alert menu 1001 queries 2026-02-19 14:52:01 +00:00
snipe
c76a888d11 Merge pull request #18579 from uberbrady/fix_npm_install_jspdf
Careful upgrade to minimize too many version changes
2026-02-19 14:25:34 +00:00
Brady Wetherington
38c495a4ac Careful upgrade to minimize too many version changes 2026-02-19 14:11:15 +00:00
snipe
d0ce5e0c57 Merge pull request #18578 from grokability/#18435-fixed-currency-selection-selectbox
Fixed #18435 - correct option value for currency format selector
2026-02-19 12:05:01 +00:00
snipe
7a0e5b57db Fixed currency selector 2026-02-19 12:01:56 +00:00
snipe
c31f1d2cce Nicer row selected color 2026-02-19 11:42:45 +00:00
snipe
ea98ee07e5 Fixed #18577 - set url back parameter as default in form component 2026-02-19 11:30:53 +00:00
snipe
a38bfada57 Merge pull request #18572 from uberbrady/fix_cross_company_created_by_in_asset_acceptance
Fixed #18571 - fix cross-company created_by in asset acceptance
2026-02-19 11:24:28 +00:00
snipe
e5ff19aec4 Fixd required field color 2026-02-19 11:02:12 +00:00
Marcus Moore
504f63066f Add note 2026-02-18 16:34:22 -08:00
Marcus Moore
41b327529b Reference rollbar 2026-02-18 16:31:55 -08:00
Marcus Moore
e84279c402 Implement remaining tests 2026-02-18 16:17:34 -08:00
spencerrlongg
65e6519f97 adds field_encrypted to fields api response 2026-02-18 17:53:04 -06:00
Marcus Moore
dbbb3beacc Clean up 2026-02-18 15:35:57 -08:00
Marcus Moore
e8a73a8de6 Organization 2026-02-18 15:29:44 -08:00
Marcus Moore
411cbc0962 Avoid overwriting created_by and timestamps 2026-02-18 15:20:28 -08:00
Marcus Moore
b487c0e3e9 Scaffold additional tests 2026-02-18 15:20:18 -08:00
Marcus Moore
fa0e8f6e01 Improve validation 2026-02-18 15:11:52 -08:00
Marcus Moore
e25e405f74 Scaffold additional test cases and organize 2026-02-18 14:52:43 -08:00
Marcus Moore
302c4a6414 Organize test 2026-02-18 14:35:06 -08:00
Marcus Moore
d1e7f6e55e Inline 2026-02-18 14:03:28 -08:00
Marcus Moore
d12f0974df Add test for permission 2026-02-18 14:01:53 -08:00
snipe
b751fe7903 Fixed incorrect translation string 2026-02-18 16:30:37 +00:00
snipe
a6e34522eb Partially convert maintenances into new blade components 2026-02-18 16:26:13 +00:00
snipe
735e6f3471 Merge pull request #18570 from grokability/convert-consumables-to-new-blade-components
Converted consumables to new blade component
2026-02-18 16:02:32 +00:00
snipe
c9f41c950a Added copy+paste, added additional fields 2026-02-18 15:57:40 +00:00
Brady Wetherington
091c710940 Make User parameter nullable 2026-02-18 15:25:05 +00:00
snipe
276f412a3c Small tweaks to info panel 2026-02-18 15:22:46 +00:00
snipe
6f6e5c847a Converted consumables to new blade component 2026-02-18 14:44:08 +00:00
snipe
6ef0274bb3 Merge pull request #18569 from grokability/updated-csvs
Small importer improvements, added larger CSV samples
2026-02-18 14:22:23 +00:00
snipe
7ef6a72ec4 Updated CSVs 2026-02-18 14:06:33 +00:00
snipe
e2b8c69cf6 Use iconhelper for dynamic icon 2026-02-18 14:06:24 +00:00
snipe
1254d12d83 Tweaks to info panel 2026-02-18 14:06:14 +00:00
snipe
b68642a827 Added alias strings 2026-02-18 14:04:51 +00:00
snipe
c5214b976b Added string 2026-02-18 14:04:44 +00:00
snipe
e611121244 Added tag color to main importer, added aliases 2026-02-18 14:04:38 +00:00
snipe
3aa7f0b7fe Added tag_color to importer 2026-02-18 14:04:19 +00:00
snipe
3d38eee71d Added/updated icons 2026-02-18 14:04:00 +00:00
snipe
90e6e309f9 Removed redundant icon helper 2026-02-18 14:03:53 +00:00
snipe
885314b87a Added large components sample CSV 2026-02-18 12:01:59 +00:00
snipe
ef013a7026 Updated components sample 2026-02-18 12:01:48 +00:00
snipe
0353ade90e Trim off whitespace for headers 2026-02-18 12:01:34 +00:00
snipe
d90c9282ac Updated strings 2026-02-18 11:13:52 +00:00
Marcus Moore
5055aafe1d Scaffold tests 2026-02-17 17:37:24 -08:00
Marcus Moore
e68b9ea267 Validate assigned_to is integer 2026-02-17 16:51:31 -08:00
Marcus Moore
1d8ba2ec61 Add failing test 2026-02-17 16:49:32 -08:00
snipe
d56970eaa3 Merge pull request #18566 from grokability/fixed-new-vs-update-on-custom-fieldset-creation
Fixed new vs update button label on custom fieldset creation, fixed breadcrumbs on new custom fieldset creation screen
2026-02-17 22:23:06 +00:00
snipe
8b3f18ec59 Better breadcrumbs for fieldset creation 2026-02-17 22:17:38 +00:00
snipe
170d20ddb5 Fixed new vs update button on custom fieldset create vs edit 2026-02-17 22:17:27 +00:00
snipe
70407dac85 Merge pull request #18565 from grokability/#18555-component-clone
Added #18555 - ability to clone components
2026-02-17 16:22:02 +00:00
snipe
c2334d87de Merge pull request #18564 from uberbrady/fix_component_counts
Fixed #18557 - Tweak numRemaining to not re-query for un-checked-out components
2026-02-17 15:44:00 +00:00
snipe
0662e3351e Added #18555 - ability to clone components 2026-02-17 15:43:43 +00:00
Brady Wetherington
6b90b3743e Tweak numRemaining to not re-query for un-checked-out components 2026-02-17 15:27:04 +00:00
snipe
dd1d456106 Alphabetize dictionary 2026-02-17 15:21:24 +00:00
snipe
dceead8302 Show footer by default if the transformer asks for it 2026-02-17 15:09:29 +00:00
snipe
832f449868 Merge pull request #18561 from grokability/#18512-comment-out-first-checkout
Fixes #18512 (first checkout) temporarily
2026-02-17 14:18:05 +00:00
snipe
33a104f142 Fixes #18512 temporarily 2026-02-17 14:11:14 +00:00
snipe
28f19689ec Fixed focus color for input fields 2026-02-17 13:38:43 +00:00
snipe
ce6ee32e89 Made seats a numeric field 2026-02-17 13:38:29 +00:00
snipe
fdf42ba321 Add class to checkedout blade 2026-02-17 13:18:27 +00:00
snipe
aadd158108 Added active class to accessory tab 2026-02-17 13:18:14 +00:00
snipe
d6592819e8 Show only active licenses 2026-02-17 13:17:49 +00:00
snipe
8f34c06196 Added order_number to API for accessories, consumables, components 2026-02-17 13:08:09 +00:00
snipe
9cdc73917b Merge pull request #18537 from grokability/more-refactoring-tables-and-tabs
WIP - More refactoring of tabs, tab panes, and tables
2026-02-17 12:39:31 +00:00
snipe
2976159499 Check for parent (only really matters in locations) 2026-02-17 12:32:07 +00:00
snipe
e302ccf985 Added plain clone translation 2026-02-17 12:31:35 +00:00
snipe
08915e8607 Use isDeletable in transformers 2026-02-17 12:31:26 +00:00
snipe
32b2131bff Fixed asset model deletable check 2026-02-17 12:31:12 +00:00
snipe
50cf15fb71 Added isDeletable() method 2026-02-17 12:31:01 +00:00
snipe
f07ef6d7c5 Added restore and clone blade 2026-02-17 12:28:48 +00:00
snipe
666025d7f6 Use more generic text 2026-02-17 12:28:27 +00:00
snipe
671601365c Use new button blade components 2026-02-17 12:28:16 +00:00
snipe
263b04cd69 Merge pull request #13506 from matmair/develop-1
Fixed: wrong index reference in MoveUploadsToNewDisk.php
2026-02-17 11:08:24 +00:00
snipe
57b98ca782 Fixed copypasta 2026-02-13 19:03:44 +00:00
snipe
46df19d7cd Don’t use fixed columns on unaccepted asset report 2026-02-13 18:27:18 +00:00
snipe
fa18524223 Fixed unaccepted report to account for much older items that might not exist 2026-02-13 18:23:59 +00:00
snipe
3ebc0532ca Added isDeleteable to Accessories 2026-02-13 18:07:22 +00:00
snipe
4f59752b8b Added rtd tab icon 2026-02-13 17:30:11 +00:00
snipe
668ab221cc Removed uploads for manufacturers since we don’t have them? 2026-02-13 17:29:56 +00:00
snipe
ad90e005c4 Added history tabs 2026-02-13 17:28:07 +00:00
snipe
952b3d6884 Converted components view to blade component
This effing sucked
2026-02-13 17:27:45 +00:00
snipe
19ca1f7578 Converted to object-specific elements in indexes 2026-02-13 17:27:14 +00:00
snipe
ef02fab94b Added checkout blade 2026-02-13 17:22:35 +00:00
snipe
144b5e7558 Switch to accessory table on accessory view 2026-02-13 17:22:22 +00:00
snipe
aff0a60138 Added presenter for component history 2026-02-13 17:22:07 +00:00
snipe
92a641f01a Added snipe_component because wtf laravel 2026-02-13 17:21:59 +00:00
snipe
929132ba07 Merge pull request #18544 from uberbrady/fix_1001_query_on_available_models_for_notification
Added some withCount() params to tweak the 'minimum number of assets' notification
2026-02-12 21:37:10 +00:00
Brady Wetherington
2a1a4a1c72 Added some withCount() params to tweak the 'minimum number of assets' notification 2026-02-12 21:32:30 +00:00
snipe
4d73894cd2 Merge pull request #18542 from marcusmoore/fixes/fd-53577-reminder-subject-2
Updated subject line of acceptance reminder emails
2026-02-12 21:22:55 +00:00
snipe
e49e805b2b Merge pull request #18539 from Godmartinz/L7163_title_adjustment
Fixes title alignment to fit label L7163_A
2026-02-12 20:43:52 +00:00
snipe
05f3bf633e Fixed typeError for isManagerOf in manager view of EULAs 2026-02-12 20:21:15 +00:00
Marcus Moore
4d5b3548d6 Update introduction line 2026-02-11 16:28:17 -08:00
Marcus Moore
00408f0103 Revert "Put checkout_qty on checkoutable so accessory and consumable emails are formatted correctly"
This reverts commit 548cceee18.
2026-02-11 16:06:27 -08:00
Marcus Moore
548cceee18 Put checkout_qty on checkoutable so accessory and consumable emails are formatted correctly 2026-02-11 13:39:37 -08:00
Marcus Moore
7eb536f80e Bump debugbar to v4.0.7 2026-02-11 11:37:57 -08:00
Marcus Moore
d91ce88e9a Merge branch 'develop' into debugbar 2026-02-11 11:37:02 -08:00
Godfrey M
76d1a20e21 adjust label L7163_A to fit title properly 2026-02-11 11:02:49 -08:00
snipe
5b6951b88d Tightened up licenses index list 2026-02-11 15:22:58 +00:00
snipe
56313e4436 Covert assets table 2026-02-11 14:54:30 +00:00
snipe
7810ae74d1 Fixed translation string 2026-02-11 14:30:42 +00:00
snipe
3ac1012757 Set fixed number for assets 2026-02-11 14:30:32 +00:00
snipe
91aec08ce0 More refactoring of tabs, tab panes, and tables 2026-02-11 13:18:32 +00:00
snipe
30970cc7f2 A few more disabled/readonly state color fixes 2026-02-11 11:21:53 +00:00
snipe
985c027b04 Fixed disabled select2 color 2026-02-11 11:12:33 +00:00
snipe
d1c4b055a9 Merge pull request #18535 from grokability/rename-field-in-infobox
Update the name of the object within the side panel
2026-02-11 10:20:23 +00:00
snipe
3f2f508e49 Fixed failing test 2026-02-11 10:16:44 +00:00
snipe
07513cb559 Update the name of the object within the side panel
It’s not always a contact/person, so…
2026-02-11 10:02:20 +00:00
snipe
66022902b7 Small style tweaks to info-box 2026-02-11 09:57:43 +00:00
snipe
613efe963a Use consistent box icon 2026-02-11 09:48:44 +00:00
snipe
5a0affcd8e Updated categories view 2026-02-11 09:47:38 +00:00
snipe
79150edb92 Fixed variable name 2026-02-11 07:41:13 +00:00
snipe
6cca24be8e Fixed typo 2026-02-11 07:27:16 +00:00
snipe
c2ca51e8ef Added edit buttons to supplier view 2026-02-11 07:27:08 +00:00
snipe
b9908d5665 Cast the DB_PORT to integer 2026-02-11 07:01:03 +00:00
snipe
cbca420217 Added tooltips 2026-02-11 06:42:15 +00:00
snipe
017183e3fe Merge pull request #18529 from grokability/consolidate-customfieldset-edit
Consolidated custom fieldset edit/show
2026-02-11 05:25:27 +00:00
snipe
0236527f05 Use correct seeding color for links 2026-02-10 15:27:05 +00:00
snipe
e946c4bf8c Consolidated custom fieldset edit/show 2026-02-10 15:20:57 +00:00
snipe
8a0414cef6 Merge remote-tracking branch 'origin/master' into develop 2026-02-10 14:16:42 +00:00
snipe
ee1ad692a4 Tweaked disabled button color 2026-02-10 13:40:14 +00:00
snipe
fdd5f6b0e1 Fixed #18497 - Added model name to aliases 2026-02-10 13:07:41 +00:00
snipe
dca6df3244 Fixed #18527 - disable sticky column on dashboard tables 2026-02-10 11:31:25 +00:00
snipe
0874b853a0 Fixed #18520 - use plain_text_company 2026-02-10 11:10:48 +00:00
snipe
3393916b5e Merge pull request #18524 from ubc-cpsc/fix/subpath-livewire-prefix
Fix Livewire and Passport routes for subpath hosting
2026-02-10 08:34:18 +00:00
Joël Pittet
b2728b4eb1 Fix Livewire routes for subpath hosting 2026-02-10 00:22:13 -08:00
snipe
f3cc3ed682 Merge pull request #18470 from bilias/pr-clean
Do not delete asset name if update request does not have a name
2026-02-09 20:57:07 +00:00
snipe
f8cfb8833f Merge pull request #18514 from grokability/update-models-with-new-components
Update models with new components
2026-02-09 19:53:40 +00:00
snipe
c2c2332e83 Removed erroneous icon 2026-02-09 13:05:22 +00:00
snipe
9f69c36426 Added depreciations and departments 2026-02-09 13:03:07 +00:00
snipe
ea9de35a3b Show/hide on companies and depts 2026-02-07 18:44:37 +00:00
snipe
a50a16fb01 Nicer toggle, nicer show/hide info button 2026-02-07 18:39:42 +00:00
snipe
f27c0206de Expand/contract info tab (still looks a little junky) 2026-02-07 17:52:22 +00:00
snipe
6791ddd911 Merge remote-tracking branch 'origin/develop' 2026-02-07 15:55:20 +00:00
snipe
ce95060d60 Fixed #18516 - added kits for sticky columns 2026-02-07 15:55:11 +00:00
snipe
6d9bbe1ddf Small tweaks to EULA API 2026-02-07 15:41:51 +00:00
snipe
5b1507f4b7 Updated license presenter 2026-02-07 15:16:32 +00:00
snipe
53e985aaab Tweaked color for alt striping again 2026-02-07 15:08:51 +00:00
snipe
f5c2119122 Merge remote-tracking branch 'origin/develop' 2026-02-07 15:03:58 +00:00
snipe
5fd6918948 Fixed text color in light mode on alt striping 2026-02-07 15:03:50 +00:00
Marcus Moore
7423d13bdd Accept $firstTimeSending in mails 2026-02-05 17:06:36 -08:00
Marcus Moore
fb8586f186 Properly pass parameter for asset emails 2026-02-05 16:59:49 -08:00
Marcus Moore
8b9ebf736b Add assertions for subject line 2026-02-05 16:59:36 -08:00
Marcus Moore
5abcdb8c5a Fix relationships in AccessoryCheckout model 2026-02-05 16:46:51 -08:00
Marcus Moore
e549f67fcc Create AccessoryCheckout factory 2026-02-05 16:45:25 -08:00
Marcus Moore
d68576c2fa Make test name more accurate 2026-02-05 13:57:51 -08:00
Marcus Moore
a6042b6a03 Clean up 2026-02-05 13:52:15 -08:00
Marcus Moore
d1a3afd992 More test clean up 2026-02-05 13:39:47 -08:00
Marcus Moore
34b00f7dba More test clean up 2026-02-05 13:38:05 -08:00
Marcus Moore
14b6c6861f Test clean up 2026-02-05 13:34:09 -08:00
Marcus Moore
fe4bb4209c Split out tests 2026-02-05 13:07:41 -08:00
Marcus Moore
e997eb2012 Inline method 2026-02-05 12:57:42 -08:00
snipe
8337473f5a Added box arrows 2026-02-05 19:27:36 +00:00
Marcus Moore
7ada7fb327 Merge branch 'develop' into livewire4 2026-02-05 10:39:56 -08:00
Marcus Moore
9d632e39bf Merge branch 'develop' into debugbar 2026-02-05 10:33:13 -08:00
snipe
1f50eada6d Added bulk edit to department user listing 2026-02-05 14:38:46 +00:00
snipe
38e1114dad Convert manufacturers and departments 2026-02-05 14:35:01 +00:00
snipe
11be73a578 Removed stray closing div 2026-02-05 14:14:25 +00:00
snipe
13a00df73c Use new components for company view 2026-02-05 14:13:51 +00:00
snipe
7739690bf5 Extend SnipeModel for custom fieldsets 2026-02-05 13:41:26 +00:00
snipe
389eb9e05d Bumped hash 2026-02-05 13:25:53 +00:00
snipe
98fe94aa24 Bumped hash 2026-02-05 13:25:21 +00:00
snipe
4ee378bf8e Merge remote-tracking branch 'origin/master' into develop
# Conflicts:
#	public/css/dist/bootstrap-table.css
#	public/js/dist/bootstrap-table.js
#	resources/views/blade/table/index.blade.php
#	resources/views/models/custom_fields_form.blade.php
2026-02-05 13:24:27 +00:00
snipe
a8c268760b Fixed #18500 - skip encoding on API url for table component 2026-02-05 13:19:16 +00:00
snipe
c684b8ab1e Fixed #18502 - custom fields disappearing on smaller resolution breakpoints 2026-02-05 12:25:49 +00:00
snipe
333d0e2391 Fixed #18510 - use correct GH link in social footer 2026-02-05 11:28:29 +00:00
snipe
c22c5993fc Fixed #18503 - alt row colors on striped tables 2026-02-05 11:27:10 +00:00
snipe
1c239cc7cf Fixed typo in icon name 2026-02-05 10:16:24 +00:00
snipe
6ee5aa3e12 Fixed checkout to all modal 2026-02-04 20:55:38 +00:00
snipe
0d5ceb1e90 Merge pull request #18492 from marcusmoore/bump-phpunit
Bumped PHPUnit version
2026-02-04 20:48:06 +00:00
snipe
166a241761 Merge pull request #18484 from spencerrlongg/undefined_var_files
Fixes `Undefined variable $files` Exception
2026-02-04 20:47:02 +00:00
snipe
f6f5e806e4 More flexibility in blade components for labels vs icons vs icon types 2026-02-04 20:31:11 +00:00
snipe
f0f42240f3 Still more icons 2026-02-04 20:30:20 +00:00
snipe
686afc0974 Gate check linking in presenters 2026-02-04 20:30:12 +00:00
Marcus Moore
5d085469ec Bump debugbar to v4.0.6 2026-02-04 10:46:34 -08:00
Marcus Moore
7c54429c13 Bump Livewire to v4.1.2 2026-02-03 09:14:41 -08:00
Marcus Moore
8dfd04573e Bump debugbar to v4.0.5 2026-02-02 09:53:17 -08:00
snipe
7e5fb3c9ae Added more icons 2026-02-02 15:20:21 +00:00
snipe
65e4c0b8d1 Renamed contact box to info-panel 2026-02-02 12:22:20 +00:00
snipe
b1301237f1 Removed double lines in table 2026-02-02 11:35:09 +00:00
snipe
cf82c12708 Fixed custom fieldset table colors 2026-02-02 11:27:18 +00:00
snipe
52f8697d91 Added icons, more additions to sidebar 2026-02-02 11:16:53 +00:00
Marcus Moore
1aee8679d2 Fix updating bug in oauth clients 2026-01-28 12:46:57 -08:00
Marcus Moore
3169c5b503 Bump phpunit 2026-01-28 10:15:11 -08:00
snipe
741f0d69ab Merge pull request #18490 from Godmartinz/add-label-preview-translation
Add translation and template name for Label Preview
2026-01-28 13:14:54 +00:00
Marcus Moore
8829ea9b29 Update category edit form 2026-01-27 17:08:21 -08:00
Godfrey M
53b283eac5 add seperate brackets for template name 2026-01-27 16:21:06 -08:00
Godfrey M
1234b6297e add Label preview translation, also which label is previewed 2026-01-27 14:59:44 -08:00
Marcus Moore
6ba99e8199 Bump livewire to v4.1.0 2026-01-27 14:03:52 -08:00
Marcus Moore
b4eb603f22 Switch blur to change 2026-01-27 13:58:42 -08:00
Marcus Moore
62f9ce979d Fix js 2026-01-27 13:41:57 -08:00
Marcus Moore
f5928fc707 Fix tags in importer 2026-01-27 12:33:46 -08:00
Marcus Moore
f7040e3616 Remove console.log 2026-01-26 17:43:47 -08:00
Marcus Moore
a70f1cc1ef Migrate request hook 2026-01-26 17:38:01 -08:00
Marcus Moore
a86b738231 Update route 2026-01-26 17:13:56 -08:00
Marcus Moore
8a9ddba208 Use traditional component naming 2026-01-26 17:02:32 -08:00
Marcus Moore
4c727966e7 Migrate config 2026-01-26 16:59:08 -08:00
Marcus Moore
c2ac53029a Update to v4 2026-01-26 16:58:58 -08:00
Marcus Moore
5d300d85bd Update content-type assertion 2026-01-26 14:52:53 -08:00
Marcus Moore
1dc181797b Move trait to base controller 2026-01-26 14:39:48 -08:00
Marcus Moore
30d932e2e0 Re-enable exception collector 2026-01-26 14:30:03 -08:00
Marcus Moore
9fb32d33af Migrate to v4 config 2026-01-26 14:28:18 -08:00
Marcus Moore
70baa60521 Install v4 of laravel debugbar 2026-01-26 14:02:01 -08:00
Marcus Moore
8ea2784d44 Uninstall debugbar 2026-01-26 14:01:29 -08:00
Marcus Moore
2b56d82577 Move disabling debugbar to trait 2026-01-26 14:01:21 -08:00
spencerrlongg
8ff34baafa wrap $files in isset to avoid null errors 2026-01-26 15:44:23 -06:00
snipe
eeae534a37 Added a few more helpers 2026-01-25 14:33:54 +00:00
snipe
8b71049a3d Merge pull request #18482 from grokability/move-form-components
Move form components into their own directory
2026-01-24 21:13:07 +00:00
snipe
373361dab0 Move form components into their own directory 2026-01-24 21:06:02 +00:00
snipe
60a1141b9d Merge pull request #18480 from grokability/#more-table-components
Moves more indexes to blade components
2026-01-24 20:55:51 +00:00
snipe
a38c8a0235 Merge more into contact card 2026-01-24 20:37:08 +00:00
snipe
c39d165a3d Added space 2026-01-24 20:36:50 +00:00
snipe
2238f8e8ad More icons 2026-01-24 20:36:43 +00:00
snipe
e0c53c3ead Merge classes 2026-01-24 18:32:13 +00:00
snipe
1074bc2d3b More conversions 2026-01-24 18:27:06 +00:00
snipe
4b22b1c115 Better defaults in nav-item component 2026-01-24 17:41:02 +00:00
snipe
141794caf7 Use new contact panel in suppliers view 2026-01-24 17:40:41 +00:00
snipe
d5997e2394 Added new classes 2026-01-24 17:40:29 +00:00
snipe
429ca8dd34 Added info components 2026-01-24 17:40:18 +00:00
snipe
c2ad0defe0 Added contact component 2026-01-24 17:39:53 +00:00
snipe
b4658f2696 Added address presenter 2026-01-24 17:39:44 +00:00
snipe
11b7dfc9b0 Added icons 2026-01-24 17:39:33 +00:00
snipe
f7b7ef850d Updated suppliers view 2026-01-24 12:10:58 +00:00
snipe
bb9b145519 Updated helper icon 2026-01-24 12:10:50 +00:00
snipe
d75f5b8fd3 Fixed label 2026-01-23 19:14:29 +00:00
snipe
34fe64b27c Fixed spacing 2026-01-23 19:09:48 +00:00
snipe
2151595b45 Fixed a few typos 2026-01-23 18:28:49 +00:00
snipe
b2c94386b3 Switched from stupid layouts/edit-form to new form component 2026-01-23 18:26:09 +00:00
snipe
b99ae9be88 Form component (will move the other things later) 2026-01-23 18:25:49 +00:00
snipe
ffbc831071 Include the box footer if there is a route passed 2026-01-23 18:25:34 +00:00
snipe
302a60fbe7 Better breadcrumb 2026-01-23 18:25:06 +00:00
snipe
3ec8ba01d0 Updated manufacturer with new tabs 2026-01-23 18:24:50 +00:00
snipe
2048eb7d5a Removed unneeded presenter methods 2026-01-23 17:08:25 +00:00
snipe
4d605927f3 Fixed typos 2026-01-23 17:07:38 +00:00
snipe
b7ee3a2f1d Updated groups index to use new components 2026-01-23 17:05:28 +00:00
snipe
44fa9e2d1e Handle status label index 2026-01-23 16:58:42 +00:00
snipe
a5e818f970 More moved to new box+table method 2026-01-23 16:47:27 +00:00
snipe
066cf81233 Added model bulk component 2026-01-23 16:46:52 +00:00
snipe
1b4552cf30 Refactorered bulk action component 2026-01-23 16:46:42 +00:00
snipe
0ba8ee1c5f Added bulkactions named slot in box index 2026-01-23 16:45:48 +00:00
snipe
5c4f3a5aec Removed unneeded body blade 2026-01-23 16:45:30 +00:00
snipe
c963c0b25b Updated path for new table component location 2026-01-23 15:45:36 +00:00
snipe
b3a4fb2676 Use new user bulk edit blade 2026-01-23 15:45:19 +00:00
snipe
0b82902248 Fixed background loading color 2026-01-23 15:45:06 +00:00
snipe
e7f70ccd1f Moved to new component structure 2026-01-23 15:44:52 +00:00
snipe
683395599f Moved files into table directory 2026-01-23 15:44:12 +00:00
snipe
755d7c2351 Added printIgnore, reordered fields 2026-01-23 15:43:35 +00:00
snipe
17d91bcd8e Merge pull request #18479 from grokability/improved-tab-components
Removed font-size override on tabs
2026-01-23 13:53:33 +00:00
snipe
2633ec10dc Removed font-size override on tabs 2026-01-23 13:52:00 +00:00
snipe
c4f772c8d9 Merge pull request #18478 from grokability/refined-sticky-columns
Added sticky column
2026-01-23 13:50:49 +00:00
snipe
18addf2a87 Added sticky column 2026-01-23 13:49:09 +00:00
snipe
eee826248d Merge pull request #18476 from marcusmoore/fixes/18475-action-log-checkin-during-import
Fixed #18475 - reference the correct model when checking in an asset via import
2026-01-23 09:37:50 +00:00
Marcus Moore
96a817753c Pass the assigned model to the event instead of always passing a User 2026-01-22 14:24:37 -08:00
snipe
600b06e66b Merge pull request #18474 from grokability/move-box-index
Renamed box blade component wrapper to index
2026-01-22 21:10:53 +00:00
snipe
cd680daa4c Merge remote-tracking branch 'origin/develop' 2026-01-22 20:58:42 +00:00
snipe
315b716e87 Removed advanced search from files table
It’s not wired up on the backend, so…
2026-01-22 20:50:24 +00:00
snipe
b2f1966a78 Renamed box blade component wrapper to index 2026-01-22 20:41:25 +00:00
snipe
6305f87037 Merge pull request #18473 from grokability/added-tab-component
Added tab components
2026-01-22 20:37:11 +00:00
snipe
ab02b67d3c Merge remote-tracking branch 'origin/develop' 2026-01-22 20:34:51 +00:00
snipe
0f4086eaf0 Added gates to the tab panes as well 2026-01-22 20:32:33 +00:00
snipe
e7c5329e57 Merge pull request #18472 from Godmartinz/fix-n+1-issue
Moved helper query, to eager load through Asset API index
2026-01-22 20:27:44 +00:00
snipe
ad82ea86c8 Added tab components 2026-01-22 20:25:33 +00:00
Godfrey M
c310e1e3b7 update blade 2026-01-22 11:37:24 -08:00
Godfrey M
aaf9372474 move helper query to eager load in the asset api 2026-01-22 11:12:49 -08:00
snipe
2cf169359e Missed updating the closing tags 2026-01-22 15:45:53 +00:00
snipe
7f67f8c20d Merge pull request #18471 from grokability/move-box-blades-into-own-directory
Moved anonymous box blade components into their own directory
2026-01-22 15:44:10 +00:00
snipe
5607dfcecb Moved anonymous box blade components into their own dir 2026-01-22 15:38:20 +00:00
Giannis Kapetanakis
ca99f525c9 Do not delete asset name if update request does not have a name 2026-01-22 12:40:59 +02:00
snipe
7e92517d13 Merge remote-tracking branch 'origin/develop' 2026-01-22 08:46:46 +00:00
snipe
1ebb67b2e7 Nicer route formatting for maintenances 2026-01-22 08:45:38 +00:00
snipe
d9ef7b43b0 Merge remote-tracking branch 'origin/develop' 2026-01-21 19:59:59 +00:00
snipe
c074fae885 Merge pull request #18459 from Godmartinz/moar_label_fixes
Fixes #18353 Label title scales with label fields. adjusted label row allotment.
2026-01-21 19:58:32 +00:00
snipe
c5ada0fc2f Merge pull request #18466 from marcusmoore/eager-load-expiring-alerts-command
Added eager load to Expiring Alerts command
2026-01-21 19:57:44 +00:00
snipe
5268b0f67f Merge pull request #18456 from grokability/container-component-phase-2
Next step in container+box component
2026-01-21 19:57:32 +00:00
Marcus Moore
7e10089c13 Eager load assignedTo and supplier 2026-01-21 11:49:02 -08:00
Godfrey M
1dab36da2d replaced extracted variables with array 2026-01-21 10:32:26 -08:00
snipe
fab50e53b8 Merge pull request #18458 from marcusmoore/fixes/17309-asset-eol-in-custom-report
Fixed #17309 - include EOL date in custom asset report
2026-01-21 11:45:08 +00:00
Godfrey M
10cfe6d37a remove duplicate parameter 2026-01-20 16:23:36 -08:00
Godfrey M
3718ce9749 if label is null make room for value 2026-01-20 16:18:13 -08:00
Godfrey M
fd39c8bf11 update title help text 2026-01-20 16:09:04 -08:00
Godfrey M
2e122fa8d8 update LW 11354 2026-01-20 16:00:56 -08:00
Godfrey M
66d85d17d9 update LW 1933081 2026-01-20 16:00:24 -08:00
Godfrey M
61bc570d59 add title to layout helper, update lw2112283 2026-01-20 15:59:40 -08:00
Marcus Moore
62dbd400a4 Use asset_eol_date in custom asset report 2026-01-20 15:07:19 -08:00
snipe
74af52d29d Merge pull request #18457 from marcusmoore/fixes/api-image-upload
Fixed storing image for accessory and consumable creation via api
2026-01-20 22:28:04 +00:00
Marcus Moore
59f377b058 Prepare images prior to validation in accessory and asset creation requests 2026-01-20 13:38:01 -08:00
snipe
949f65b210 Merge pull request #18356 from Godmartinz/add-first-checkout-to-asset
Adds #17210 1st checkout to asset view and index
2026-01-20 20:36:21 +00:00
snipe
4046fbae89 Merge pull request #18256 from iryadifarhan/fix/audit-log-displays-negative-number-incorrectly-when-next-audit-date-filled-with-current-date
Fixes inconsistent negative symbol at Audit Log Report when Next Audit Date is set to the current date
2026-01-20 20:35:16 +00:00
snipe
da8776c2f1 Larger group select box 2026-01-20 20:20:39 +00:00
snipe
4043df1d02 Sort groups by name, asc 2026-01-20 20:13:22 +00:00
snipe
3bc5ab593a Next step in container+box component 2026-01-20 14:49:09 +00:00
snipe
6dad0d669f Merge pull request #18454 from grokability/container-components
Use basic container+box for primary index pages with one column
2026-01-19 16:54:45 +00:00
snipe
39f581a826 Use basic container+box for primary index pages with one column 2026-01-19 16:47:55 +00:00
snipe
2034b25b25 Set footer to default false 2026-01-19 16:20:48 +00:00
snipe
468a7aa911 Added button component 2026-01-19 16:19:07 +00:00
snipe
ec50643d96 Basic container+box components 2026-01-19 16:17:47 +00:00
snipe
3b98fb666f Added tel as form type to make look right 2026-01-19 16:15:14 +00:00
snipe
f17eeed579 Added fax icon 2026-01-19 15:09:28 +00:00
snipe
452d0b6f6e Merge remote-tracking branch 'origin/develop' 2026-01-19 11:44:04 +00:00
snipe
41450b6e1b Merge pull request #18368 from marcusmoore/feature/group-create-edit-load-improvement
Improved loading speeds on group create and edit page
2026-01-19 11:38:00 +00:00
snipe
30029462b1 Merge remote-tracking branch 'origin/develop' 2026-01-17 11:40:22 +00:00
snipe
e109879cac Small autolabeler improvements 2026-01-17 11:40:08 +00:00
snipe
90ad4e9abf Merge remote-tracking branch 'origin/develop' 2026-01-17 11:27:43 +00:00
snipe
fd0e2b1a96 Fixed indenting and page footer 2026-01-17 11:27:34 +00:00
snipe
19d637efea Merge remote-tracking branch 'origin/develop' 2026-01-16 13:03:26 +00:00
snipe
6c18b35276 Added barcode class 2026-01-16 13:03:17 +00:00
snipe
184eddb5cf Merge remote-tracking branch 'origin/develop' 2026-01-16 12:52:43 +00:00
snipe
1d577b0171 Merge pull request #18450 from grokability/#18449-small-sidenav-fixes
Fixed #18449 - small sidenav improvements for selected contexts
2026-01-16 12:51:52 +00:00
snipe
05c998227a Fixed #18449 - small sidenav improvements for selected contexts 2026-01-16 12:48:28 +00:00
snipe
53ed810d86 Merge remote-tracking branch 'origin/develop' 2026-01-16 11:38:09 +00:00
snipe
7c136b6f57 Use fa-fw in footer icons 2026-01-16 11:37:57 +00:00
snipe
4e510d9d8c Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	resources/views/models/index.blade.php
2026-01-16 11:36:35 +00:00
snipe
e1f30f96c9 Remove stray closing divs 2026-01-16 11:36:10 +00:00
snipe
146d0cefc0 Removed extra closing divs 2026-01-16 11:35:18 +00:00
snipe
0103bd58e1 Merge pull request #18434 from Godmartinz/label_lay_fix
Fixes Label text scaling
2026-01-15 21:13:01 +00:00
Godfrey M
dbc1e7d6ab remove scaling from label title 2026-01-15 12:03:45 -08:00
snipe
ea706863b5 Merge remote-tracking branch 'origin/develop' 2026-01-15 11:42:18 +00:00
snipe
04f4f5b57c Merge pull request #18439 from grokability/#18135-better-handle-cli-importer-permissions
Fixed #18135 - only unset sensitive variables in the web UI importer
2026-01-15 11:39:03 +00:00
snipe
98b9246c10 Merge pull request #18442 from uberbrady/fewer_scim_exceptions
Fixed - throw fewer exceptions on SCIM misconfigurations
2026-01-15 11:38:49 +00:00
snipe
1e158721ee Merge remote-tracking branch 'origin/develop' 2026-01-15 11:10:09 +00:00
snipe
ab4eefcac2 Added links to snipeit-mcp (by @jameshgordy) and SnipeScheduler (by @JSY-Ben) 2026-01-15 11:09:50 +00:00
snipe
cf3b36f124 Merge remote-tracking branch 'origin/develop' 2026-01-15 09:45:40 +00:00
snipe
8da01b8569 Check for ID in ldap sync UI screen 2026-01-15 09:45:29 +00:00
snipe
c90afaa312 Fixed quotes in cli purge eula command 2026-01-15 09:10:31 +00:00
snipe
9f20bb89c1 Merge remote-tracking branch 'origin/develop' 2026-01-14 21:37:54 +00:00
snipe
dfe664b779 Merge pull request #18437 from grokability/#18409-purge-signature-pdfs
Fixed #18409 - command line option to purge signatures
2026-01-14 21:37:35 +00:00
snipe
88bd1fd6ef Merge pull request #18387 from vasiliyplotnikov/mysql_rds_ssl_support_18312
Fixed #18312: support aws rds mysql with force tls
2026-01-14 21:23:18 +00:00
snipe
33f8823edb Merge remote-tracking branch 'origin/develop' 2026-01-14 21:17:28 +00:00
snipe
06d5c0a86c Merge pull request #18440 from ManiacTwister/fix-importer-logfile
Fixed #8740: Use log instance with configured log path
2026-01-14 21:17:09 +00:00
snipe
e12a5eda01 Merge remote-tracking branch 'origin/develop' 2026-01-14 20:56:44 +00:00
snipe
b3c59b2cc3 Use readonly style with light/dark on LDAP settings 2026-01-14 20:56:35 +00:00
Brady Wetherington
24d5969ce8 Bump versions of our branch of laravel-scim-server to reduce reollbar usage 2026-01-14 14:37:47 +00:00
snipe
6ad42a02ad Merge remote-tracking branch 'origin/develop' 2026-01-14 14:21:50 +00:00
snipe
90b1ee4805 Check that signature exists on the server before trying to display 2026-01-14 14:21:41 +00:00
snipe
0c9d5ca9da Merge remote-tracking branch 'origin/develop' 2026-01-14 14:13:53 +00:00
snipe
e05ecd0c3e Add created_by on acceptance logging to action_log 2026-01-14 14:13:43 +00:00
snipe
441657cbfb Merge remote-tracking branch 'origin/develop' 2026-01-14 13:48:54 +00:00
snipe
0fb6b02fc4 Merge pull request #18418 from Godmartinz/rb20479
Fix [RB-20479] Checkin/Checkout notifications from logging as errors
2026-01-14 13:37:48 +00:00
snipe
f83df0d651 Merge remote-tracking branch 'origin/develop' 2026-01-14 13:34:29 +00:00
snipe
e667cd20a8 Set require_signature to default to 0 2026-01-14 13:30:54 +00:00
ManiacTwister
d06c56367f Fixed #8740: Use log instance with configured log path 2026-01-14 14:23:42 +01:00
snipe
039b4cf19f Fixed #18135 - only unset variables if the user is authenticated (Web UI) 2026-01-14 13:10:46 +00:00
snipe
ca0961bd49 Clarified text 2026-01-14 12:50:57 +00:00
snipe
79528fa87b Fixed #18409 - command line option to purge signatures 2026-01-14 12:45:18 +00:00
Godfrey M
d96d0b1bcb applied to LabelWriter_11354 2026-01-13 12:11:30 -08:00
Godfrey M
02c237404e applied to TZe_24mm_E 2026-01-13 12:07:39 -08:00
Godfrey M
c56f5282ff applied to TZe_241 2026-01-13 12:02:05 -08:00
Godfrey M
d62f10cb46 apply helper to L4736_A 2026-01-13 11:57:53 -08:00
Godfrey M
4c6f123cda apply helper to L6009_A 2026-01-13 11:55:10 -08:00
Godfrey M
b0b1829426 add Helper function for label layout, applied to 1933081 2112283" 2026-01-13 11:50:14 -08:00
snipe
68b590c263 Merge pull request #18419 from Godmartinz/rb20638
Refactor the exceptions for Audit log (again)
2026-01-13 18:20:20 +00:00
Godfrey M
b45efddd9a readd connection and throwable 2026-01-13 08:56:12 -08:00
snipe
d8f5d8c6ec Merge remote-tracking branch 'origin/develop' 2026-01-13 13:34:18 +00:00
snipe
56baa8ac9f Fixed manager view label alignment 2026-01-13 13:34:09 +00:00
snipe
15aa64ed28 Merge remote-tracking branch 'origin/develop' 2026-01-13 13:30:52 +00:00
snipe
6d0bfeb420 Fixed #18422 - Hide edit buttons if the manager is looking at a different profile 2026-01-13 13:30:42 +00:00
snipe
ceff334420 Merge remote-tracking branch 'origin/develop' 2026-01-13 12:43:25 +00:00
snipe
4ee2db68fc Fixed #18429 - corrected string for requestable vs requested items 2026-01-13 12:43:07 +00:00
snipe
1f04d0023b Merge remote-tracking branch 'origin/develop' 2026-01-13 12:38:43 +00:00
snipe
4427fdcaec Use theme button for LDAP sync 2026-01-13 12:38:33 +00:00
snipe
4de642c6a4 Merge pull request #18430 from grokability/#18415-null-ldap-display-name
Fixed #18415 - LDAP sync improvements to allow null display_name and bulk editing display_name
2026-01-13 12:37:48 +00:00
snipe
d3eb89a97c Fixed display name in user table 2026-01-13 12:01:34 +00:00
snipe
d7362a3785 Nicer light/dark for ldap sync page 2026-01-13 12:01:12 +00:00
snipe
26347ac41e Add display name to bulk user edit 2026-01-13 12:00:46 +00:00
snipe
b396da3f33 Use null instead of blank if value is empty 2026-01-13 12:00:27 +00:00
snipe
306a0bf6de Merge remote-tracking branch 'origin/develop' 2026-01-13 10:22:38 +00:00
snipe
8000e274c6 Fixed #18424 - adds BYOD to view-assets page 2026-01-13 10:22:19 +00:00
akemidx
5d91e0fa0a name box alignment/icons for shared or not 2026-01-12 07:48:53 -05:00
akemidx
eb41247f2d changing 'shared_report_template' to 'is_shared' in DB part 2, also last commit deleted an unused request file 2026-01-08 18:54:44 -07:00
akemidx
d53eafe87a changing 'shared_report_template' to 'is_shared' in DB 2026-01-08 18:52:36 -07:00
Godfrey M
e48c40e5af remove use statements 2026-01-08 15:58:15 -08:00
Godfrey M
df51318fb9 remove throwable exception..too vague 2026-01-08 15:55:30 -08:00
Marcus Moore
065c17002e Remove unused test cases 2026-01-08 15:18:16 -08:00
Marcus Moore
0e46a54013 Improve input alignment 2026-01-08 15:08:56 -08:00
Marcus Moore
0e35fb941b Improve assertions 2026-01-08 14:17:10 -08:00
Marcus Moore
71c9b927c6 Allow using same template name 2026-01-08 14:17:00 -08:00
Godfrey M
0327d01287 typo fixes" 2026-01-08 10:52:27 -08:00
Godfrey M
0ab206ca13 log warnings instead of errors for 4xx status codes 2026-01-08 09:47:35 -08:00
akemidx
e42960ea15 sharing a report template test 2026-01-07 18:49:34 -07:00
Marcus Moore
636d42a52e Make test more explicit 2026-01-07 16:42:47 -08:00
Marcus Moore
1451843f84 Implement test for viewing shared template 2026-01-07 16:39:10 -08:00
Marcus Moore
f8af21306a Handle attempting to delete another user's template 2026-01-07 16:37:34 -08:00
Marcus Moore
4e874cdb1b Fix update logic 2026-01-07 16:13:22 -08:00
akemidx
dbd9a844dc scaffold test cases around sharing templates 2026-01-07 16:29:32 -07:00
snipe
542cdef0bd Merge remote-tracking branch 'origin/develop' 2026-01-07 15:55:44 +00:00
snipe
f5955e14ff Merge pull request #18410 from grokability/#18402-saml-fields-readonly-display
Fixed #18402 - Clean up SAML readonly display
2026-01-07 15:55:25 +00:00
snipe
a0df7adbd1 Override readonly styles on user edit page 2026-01-07 15:50:01 +00:00
snipe
82a4398ef6 Removed @disabled on textarea 2026-01-07 15:37:05 +00:00
snipe
9271930ba8 Clean up SAML readonly display 2026-01-07 15:27:33 +00:00
snipe
f149f0d994 Merge remote-tracking branch 'origin/develop' 2026-01-07 13:36:31 +00:00
snipe
14ff325608 Merge pull request #18404 from kenchan0130/patch-displayname
fixed: Prefer display name of user on UI
2026-01-07 13:29:56 +00:00
Tadayuki Onishi
8e37fcc71e Since displayname should be referenced in the UI, we've changed it to use displayname 2026-01-07 21:59:27 +09:00
snipe
bf910bc708 Merge pull request #18406 from dbakan/patch-1
Fixed #18405: Clean up closing divs to fix footer position
2026-01-07 12:55:54 +00:00
Daniel Albertsen
1136ea0779 Clean up stray closing div tags
Removed redundant closing div tags in the edit view.
2026-01-07 12:43:03 +01:00
Daniel Albertsen
9ad94b6562 Fix white space indention 2026-01-07 12:43:03 +01:00
akemidx
c5284ed195 works fully, just needs to restrict editing for non template creator 2026-01-06 17:21:22 -07:00
Marcus Moore
5cd92dd794 Remove redundant display of "Category" 2026-01-05 15:13:27 -08:00
snipe
5ddb0b4a55 Merge remote-tracking branch 'origin/develop' 2026-01-05 22:29:17 +00:00
snipe
990858ba41 Merge pull request #18398 from grokability/fixes-rb-#20282-set-files-variable
Set `$files` in UploadedFilesController API POST endpoint only if there are results
2026-01-05 22:28:48 +00:00
snipe
406951fc84 Set $files only if there are results 2026-01-05 22:17:06 +00:00
snipe
41c7bf4aaa Merge pull request #18397 from Godmartinz/rb-20474-fix
Adds [RB-20474] a try/catch to ms teams audit notification
2026-01-05 20:39:42 +00:00
Godfrey M
1543634cb0 remove line break 2026-01-05 12:39:28 -08:00
Godfrey M
b935752ec0 reference endpoint appropriately" 2026-01-05 12:35:20 -08:00
Godfrey M
2a60b7b7b2 move endpoint 2026-01-05 12:31:00 -08:00
snipe
cef78687b3 Merge remote-tracking branch 'origin/develop' 2026-01-05 20:27:37 +00:00
snipe
2f29edc01f Changed “reset to default” button to theme button style 2026-01-05 20:27:25 +00:00
snipe
134491b59e Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2026-01-05 20:20:51 +00:00
snipe
812ff0bfd2 Bumped jspdf to >=4.0.0 2026-01-05 20:20:09 +00:00
Godfrey M
201c4fa0d9 remove use path 2026-01-05 12:15:37 -08:00
Godfrey M
ced83b9bfc add a try catch to ms teams audit notification 2026-01-05 12:14:42 -08:00
akemidx
bd98ee62e1 something something prebreak 2026-01-05 12:16:16 -07:00
snipe
e67c5273d7 Merge remote-tracking branch 'origin/develop' 2026-01-05 17:22:50 +00:00
snipe
b4b9339065 Merge pull request #18396 from uberbrady/fix_select2_break_on_modal
Fixed #17652 - don't break company drop-down with modals
2026-01-05 17:22:29 +00:00
snipe
f3cd68eb3f Merge remote-tracking branch 'origin/develop' 2026-01-05 17:21:44 +00:00
snipe
ad57dea0e5 Suppress refresh button on client-side tables
Refresh only works on server-side tables
2026-01-05 17:21:32 +00:00
snipe
1e0a348b8e Merge remote-tracking branch 'origin/develop' 2026-01-05 17:19:56 +00:00
snipe
9c06a2126d Update sha.js to 2.4.12 2026-01-05 17:19:43 +00:00
snipe
dab030e95d Merge remote-tracking branch 'origin/develop' 2026-01-05 16:56:02 +00:00
snipe
4995bc0d0d Small tweaks to select2 values 2026-01-05 16:55:53 +00:00
Brady Wetherington
d2369893c8 Remove ID from company select in partial 2026-01-05 16:30:47 +00:00
snipe
e48adf6443 Merge remote-tracking branch 'origin/develop' 2026-01-05 16:06:08 +00:00
snipe
505bca8386 Fixed :focus on theme buttons 2026-01-05 16:05:59 +00:00
snipe
31ca93a259 Merge remote-tracking branch 'origin/develop' 2026-01-05 15:40:59 +00:00
snipe
71523b7038 Merge pull request #18393 from grokability/#18291-bump-alpine
Attempted fix of #18291 - update PHP versions and alpine versions
2026-01-05 15:36:40 +00:00
snipe
9885dc9c8a Merge remote-tracking branch 'origin/develop' 2026-01-05 15:34:19 +00:00
snipe
00d9d9f132 Merge pull request #18395 from grokability/#18394-ldap-text-dark-mode
Fixed #18394 - LDAP sync test table background in dark mode
2026-01-05 15:34:00 +00:00
snipe
320dc3fac6 Fixed LDAP sync test table background in dark mode 2026-01-05 15:29:50 +00:00
Brady Wetherington
6e68e43a25 In this version of Alpine, php84 is default, so it's easier. 2026-01-05 15:11:08 +00:00
snipe
a19c391aa1 Check can edit on demo in bulk user edit 2026-01-05 14:58:14 +00:00
snipe
32f0101c1b Attempted fix of #18291 - update PHP versions and alpine versions 2026-01-05 14:05:47 +00:00
snipe
12b9fdced5 Merge remote-tracking branch 'origin/develop' 2026-01-05 13:39:48 +00:00
snipe
0af45a53a9 Merge pull request #18359 from marcusmoore/17816-qty-in-activity-report
Fixed #17816 - added quantity to activity reports
2026-01-05 13:03:56 +00:00
snipe
37773d35f2 Merge pull request #18391 from ubc-cpsc/fix/PKSA-8x19-j2j3-bn67-sodium_compat
fix: paragonie/sodium_compat - Missing check that a point is on the prime subgroup for Edwards25519
2026-01-05 12:58:24 +00:00
snipe
fca3eb4b7b Merge pull request #18392 from grokability/#18377-min-value-for-name
Fixes #18377 - make min value for names consistent
2026-01-05 12:56:29 +00:00
snipe
5e9e0b70db Fixes #18377 - make min value for names consistent 2026-01-05 12:51:49 +00:00
Joël Pittet
5242ffc04b fix: Missing check that a point is on the prime subgroup for Edwards25519 2026-01-02 13:55:18 -08:00
snipe
f0c9a5b2dc Merge remote-tracking branch 'origin/develop' 2025-12-31 05:31:06 +00:00
snipe
b3902e82fc Merge pull request #18388 from dylang3/fix/unintended-shell-exec-call
Fixed #18384: Remove backticks in bulk-actions.blade.php to avoid unintentional shell_exec() call
2025-12-31 05:30:06 +00:00
Dylan Guthrie
d70eff6fc4 Fix: Remove backticks in bulk-actions.blade.php to avoid unintentional shell_exec() call 2025-12-30 22:37:49 -06:00
Vasily Plotnikov
5b5695ffe1 Fixed #18312: support aws rds mysql with force tls 2025-12-30 11:59:31 +00:00
snipe
deb44cad30 Merge remote-tracking branch 'origin/develop' 2025-12-23 13:34:36 +00:00
snipe
35fdca3607 Added logo hover color 2025-12-23 13:33:53 +00:00
snipe
93080523d0 Merge remote-tracking branch 'origin/develop' 2025-12-21 08:34:42 +00:00
snipe
b7b8f5a7e7 Merge pull request #18348 from grokability/dependabot/github_actions/develop/actions/cache-5
Bump actions/cache from 4 to 5
2025-12-21 08:16:48 +00:00
snipe
59ee55a6b2 Merge pull request #18349 from grokability/dependabot/github_actions/develop/actions/upload-artifact-6
Bump actions/upload-artifact from 5 to 6
2025-12-21 08:16:29 +00:00
snipe
d85b25d683 Merge pull request #18363 from ubc-cpsc/task/request-get
fix: replace deprecated Symfony Request::get() usage
2025-12-21 08:09:50 +00:00
snipe
0108006fab Merge remote-tracking branch 'origin/develop' 2025-12-19 06:32:09 +00:00
snipe
755bb8f189 Merge pull request #18326 from dylang3/fix/users-groups-sync
Fixes #18325: Ensure existing permission group users are maintained when editing a group
2025-12-19 06:26:44 +00:00
Marcus Moore
f53dcbc64f Only pull needed fields from database 2025-12-18 13:37:56 -08:00
Joël Pittet
0f215bbcf8 fix: replace deprecated Symfony Request::get() usage 2025-12-17 18:44:04 -08:00
snipe
9770775770 Merge remote-tracking branch 'origin/develop' 2025-12-17 16:27:42 +00:00
snipe
0b63bcc056 Derp. Copypasta 2025-12-17 16:25:03 +00:00
snipe
03116f5ece Fixed tests 2025-12-17 16:16:58 +00:00
snipe
5c091d8690 DIsable delete button if user cannot delete the user 2025-12-17 15:31:57 +00:00
snipe
da1ca24190 Remove avatar delete - should be done in purge 2025-12-17 15:29:07 +00:00
akemidx
33f7a8d356 save copy 2025-12-16 21:38:13 -05:00
akemidx
64c3fe9099 notes and new direction? 2025-12-16 21:27:51 -05:00
Marcus Moore
fa7382851f Use enum 2025-12-16 15:00:07 -08:00
Marcus Moore
ab363596fd Replace qty with quantity 2025-12-16 14:55:00 -08:00
Marcus Moore
b05970acf4 Add command to migrate license seat quantities in action log table 2025-12-16 13:48:23 -08:00
snipe
aa6b70c296 Merge remote-tracking branch 'origin/develop' 2025-12-16 20:31:58 +00:00
snipe
00171e6d16 Fixed api docs url 2025-12-16 20:31:50 +00:00
snipe
061e0ded72 Merge remote-tracking branch 'origin/develop' 2025-12-16 20:26:18 +00:00
snipe
81bdd86fb7 Merge pull request #18358 from grokability/add-scim-url
Add SCIM url to admin settings
2025-12-16 20:06:50 +00:00
snipe
60ff06bcf0 Fixed nav footer label color 2025-12-16 20:02:20 +00:00
snipe
a3e3f48d47 Added scim and API links to admin page 2025-12-16 19:59:52 +00:00
Godfrey M
a3a79a696f adds first checkout to asset view and index 2025-12-16 08:48:08 -08:00
Marcus Moore
6947672046 Add seats to transformer 2025-12-15 17:31:56 -08:00
Marcus Moore
6f77e96998 Log quantity when adding or removing seats from license 2025-12-15 17:26:05 -08:00
Marcus Moore
8a595aa269 Display qty in history 2025-12-15 15:12:34 -08:00
Marcus Moore
1ccf38221f Set qty when accepting or declining checkout 2025-12-15 14:45:05 -08:00
Marcus Moore
18c639e6c0 WIP - adding qty to more event calls 2025-12-15 14:38:13 -08:00
Marcus Moore
06224371b3 Begin passing qty in CheckoutableCheckedOut event 2025-12-15 12:47:36 -08:00
dependabot[bot]
d06105f410 Bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 08:02:41 +00:00
dependabot[bot]
3226340b08 Bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 08:02:30 +00:00
snipe
25ef5d64b4 Merge remote-tracking branch 'origin/develop' 2025-12-13 13:44:34 +00:00
snipe
f286558065 Fixed #18344 2025-12-13 13:44:23 +00:00
snipe
9586c50cb5 Merge pull request #18347 from grokability/added-reset-for-theme
Added reset for theme, preview for effects in profile
2025-12-13 13:43:23 +00:00
snipe
4658bf38c5 Removed user migration for colors 2025-12-13 13:34:10 +00:00
snipe
951a4e37f3 New strings 2025-12-13 13:33:59 +00:00
snipe
6ad3154035 Added confetti, sounds test on checkboxes 2025-12-13 13:33:49 +00:00
snipe
d7fa4a0df2 Tweaked colors 2025-12-13 13:33:13 +00:00
snipe
1112a40f0f Added reset button 2025-12-13 13:33:06 +00:00
snipe
78d1256b74 Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	config/version.php
2025-12-12 08:47:16 +00:00
snipe
a3f9aad418 Bumped version 2025-12-12 08:46:25 +00:00
snipe
dba8cb83bf Merge remote-tracking branch 'origin/develop' 2025-12-12 07:49:53 +00:00
snipe
1954c607cd #18339 - removed heroku support 2025-12-12 07:49:01 +00:00
snipe
744124f407 Merge remote-tracking branch 'origin/develop' 2025-12-12 07:14:51 +00:00
snipe
3c14921a8c #18340 - jfc swift 2025-12-12 07:14:36 +00:00
snipe
b595fe7488 Merge remote-tracking branch 'origin/develop' 2025-12-12 06:41:18 +00:00
snipe
b0b194cef7 Fixed missing comma 2025-12-12 06:41:06 +00:00
snipe
eb0a3a27d3 Merge remote-tracking branch 'origin/develop' 2025-12-12 05:03:09 +00:00
snipe
72fbcd72e0 Fix for fullscreen with dark/light 2025-12-12 05:02:56 +00:00
snipe
09e660a38c Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2025-12-12 04:26:35 +00:00
snipe
add1810fcc Added more options and validation to suppliers “new” modal 2025-12-12 04:18:42 +00:00
snipe
eead2ce93e Adds cursor pointer to checkboxes and radios and their labels 2025-12-12 03:51:20 +00:00
akemidx
193fba71f3 fixing validation 2025-12-11 17:21:48 -05:00
snipe
5e60d96614 Simpler footer link color 2025-12-11 20:15:57 +00:00
snipe
85c721da99 Better alert color on dark 2025-12-11 20:14:33 +00:00
snipe
f3f09dd9a5 Upgraded jquery to 3.7.1 2025-12-11 20:12:23 +00:00
snipe
29ad804ca8 Merge pull request #18332 from Godmartinz/add-category-to-account-accept-table
Fixed #18316 - Adds #18316 category to pending acceptance index table
2025-12-11 19:12:15 +00:00
Godfrey M
a8c77d6e26 update accessor to laravel 11.x standards 2025-12-11 11:07:36 -08:00
snipe
b949380db8 Fixed highlighted button in license toolbar 2025-12-11 18:38:40 +00:00
Godfrey M
b7f6137a63 add accessor for category name on checkout acceptance model 2025-12-11 10:08:00 -08:00
snipe
181cd7f0dc Merge remote-tracking branch 'origin/develop' 2025-12-11 18:03:12 +00:00
snipe
10692dc587 Removed back button on history importer, themed upload button 2025-12-11 17:49:56 +00:00
snipe
8d0793e004 Merge remote-tracking branch 'origin/develop' 2025-12-11 17:45:23 +00:00
snipe
02da163ee0 Fixed modal header color in light mode 2025-12-11 17:45:14 +00:00
snipe
3199e94b3c Made icons fixed width via fa-fw 2025-12-11 16:49:02 +00:00
snipe
ac2a1503e2 Merge remote-tracking branch 'origin/develop' 2025-12-11 16:45:48 +00:00
snipe
ea10167607 Merge pull request #18333 from Godmartinz/fix-checkin-notification
Fixes [FD-52267] Expected Checkin Notification Shows Overdue instead of Reminder
2025-12-11 16:44:45 +00:00
snipe
e617b913cd Merge remote-tracking branch 'origin/develop' 2025-12-11 16:44:01 +00:00
snipe
8f6208a3c9 Removed text-blue in bootstrap-tables 2025-12-11 16:42:56 +00:00
snipe
39c71481c9 Added breaks 2025-12-11 16:34:55 +00:00
snipe
a38e49290e Added external link indicator to help text 2025-12-11 16:31:12 +00:00
snipe
f974427964 Merge remote-tracking branch 'origin/develop' 2025-12-11 16:05:41 +00:00
snipe
1f311c8657 One more nullsafe 2025-12-11 16:05:31 +00:00
snipe
c0406734bc Merge remote-tracking branch 'origin/develop' 2025-12-11 16:01:15 +00:00
snipe
66e80628f6 Account for no created_by value 2025-12-11 16:01:06 +00:00
akemidx
8a14800ef2 fixes/beginning validation 2025-12-11 03:42:10 -05:00
akemidx
a46d73f562 spacing/hiding when no template loaded 2025-12-11 03:21:34 -05:00
Marcus Moore
e908838376 Add failing tests 2025-12-10 15:44:51 -08:00
Marcus Moore
4b1339a11c Add qty to action_logs table 2025-12-10 13:30:34 -08:00
Godfrey M
620c43fd6d fixes expected checkin Notification 2025-12-10 11:33:08 -08:00
Godfrey M
dfb9d5622a adds category to account accept index table 2025-12-10 10:43:59 -08:00
snipe
af0aa7da4e Merge remote-tracking branch 'origin/develop' 2025-12-10 10:27:49 +00:00
snipe
75ddb50738 Use theme color for logo uploads 2025-12-10 10:27:40 +00:00
snipe
600238dd9b Merge remote-tracking branch 'origin/develop' 2025-12-10 09:38:51 +00:00
snipe
5a88e98ad9 Merge pull request #18330 from grokability/bigger-double-scrollbar-issue
Fixed #18317 - Longer double scrollbar issue
2025-12-10 09:38:37 +00:00
snipe
84a0544621 Removed <div class="table-responsive"> 2025-12-10 09:33:08 +00:00
snipe
8a1c7ee448 Remove <div class="table table-responsive"> 2025-12-10 09:09:12 +00:00
Dylan Guthrie
c978be5cab send associated_users to view as collection 2025-12-09 19:47:42 -06:00
snipe
2fb29dad0a Merge remote-tracking branch 'origin/develop' 2025-12-10 00:09:17 +00:00
snipe
7d160abdaf Merge pull request #18323 from grokability/#18317-removed-responsive-table-divs
Fixed #18317 - Double scrollbars on some screens
2025-12-10 00:08:55 +00:00
snipe
6c5d2c6716 Merge pull request #18328 from grokability/#17197-download-importer-files
Added #17197: Ability to download uploaded import CSVs
2025-12-10 00:07:01 +00:00
snipe
f3feff7988 Disable delete button if not owner of super admin 2025-12-09 23:59:22 +00:00
snipe
7d24f50cdc Removed extra whitespace 2025-12-09 23:49:21 +00:00
snipe
7c7375ed43 Undo temp delete commented out 2025-12-09 23:48:34 +00:00
snipe
e2e4adca4e Generic message if the user tries to delete a file they don’t have access to 2025-12-09 23:46:53 +00:00
snipe
a350b9bc3d Handle redirect if the user does not have permission to view results 2025-12-09 23:46:33 +00:00
snipe
7854543122 Added import-download controller 2025-12-09 23:46:13 +00:00
snipe
8b5636c0ab Override progress bar text color 2025-12-09 23:45:58 +00:00
snipe
9f948fd2ba Link to download and user 2025-12-09 23:45:49 +00:00
snipe
60fb67461a Added downnload route 2025-12-09 23:45:38 +00:00
snipe
5c896fc965 Merge pull request #18314 from Godmartinz/adjust-fonts-on-labelwriter-211xxx
Fixes [FD-52064] LabelWriter label font choice for fields, adds scaling to all labels.
2025-12-09 22:08:35 +00:00
Dylan Guthrie
242201ca91 Fix: Ensure existing permission group users are maintained when editing group 2025-12-09 16:06:49 -06:00
akemidx
865392d1f7 margin 2025-12-09 14:10:32 -05:00
akemidx
b880ed2371 front end clarity 2025-12-09 13:14:09 -05:00
snipe
c779988771 Fixed gallery cards in dark mode 2025-12-09 16:53:02 +00:00
snipe
e6eb15d053 Removed the duplicate table-responsive in BS tables 2025-12-09 16:52:51 +00:00
snipe
05b957df19 Merge pull request #18322 from grokability/bulk-checkin-delete-UI-fixes-dark-mode
Bulk checkin delete UI fixes dark mode
2025-12-09 15:38:25 +00:00
snipe
96da8a5fab Updated route 2025-12-09 15:28:22 +00:00
snipe
62bf61402e Updated strings 2025-12-09 15:24:16 +00:00
snipe
227be798f6 Visual tweaks to bulk 2025-12-09 15:24:10 +00:00
snipe
53f304d137 Updated view with tooltip on disabled 2025-12-09 15:23:59 +00:00
snipe
137d362369 Added breadcrumbs 2025-12-09 15:21:50 +00:00
snipe
5b2cf54f50 Fixed accessory buttons on mobile 2025-12-09 14:06:53 +00:00
snipe
b4bc785f7c Merge remote-tracking branch 'origin/develop' 2025-12-09 13:13:59 +00:00
snipe
98a8e4c2ec Merge pull request #18319 from uberbrady/fix_component_edit
Fixed [RB-4066], #18308 - Fix error on saving component after create
2025-12-09 13:13:45 +00:00
Brady Wetherington
bed6b04c3d Unset the 'sum_unconstrained_assets' attribute before saving 2025-12-09 13:00:41 +00:00
Marcus Moore
def04017e0 Improve readability in tests 2025-12-08 14:10:44 -08:00
Marcus Moore
9eeb916796 Improve clarity in test 2025-12-08 14:04:18 -08:00
Marcus Moore
b06a0c5d83 Use value already computed 2025-12-08 13:59:07 -08:00
Marcus Moore
90541ba349 Use foreach instead of reduce 2025-12-08 13:57:38 -08:00
Marcus Moore
0cc346259b Use foreach instead of reduce 2025-12-08 13:51:30 -08:00
Marcus Moore
98c343b438 Improve method ordering 2025-12-08 13:48:22 -08:00
akemidx
7ee0cdc6c7 hide edit and delete for non report creators 2025-12-08 16:42:38 -05:00
akemidx
6a770832ba only creator can see share checkbox 2025-12-08 16:32:11 -05:00
akemidx
ef0ff65162 scoping shared templates 2025-12-08 16:29:14 -05:00
akemidx
8c0e7e1bb3 share option saved in db column 2025-12-08 15:55:26 -05:00
snipe
babb3ffb9c Merge remote-tracking branch 'origin/develop' 2025-12-08 20:24:31 +00:00
snipe
15c96f753c Revert hasMany “fix” 2025-12-08 20:22:55 +00:00
snipe
354bdeffbf Merge remote-tracking branch 'origin/develop' 2025-12-08 20:19:54 +00:00
snipe
512af90d31 Re-removed non-asset models from kits
These still do not work as expected.
2025-12-08 20:18:39 +00:00
Marcus Moore
8cb2ef7cac Add typehint 2025-12-08 12:17:04 -08:00
Marcus Moore
046b38e5c2 Improve method name 2025-12-08 12:10:25 -08:00
Marcus Moore
d50d7fd631 Account for null value 2025-12-08 12:09:58 -08:00
snipe
ed837b7527 Merge remote-tracking branch 'origin/develop' 2025-12-08 19:33:07 +00:00
snipe
fa5dd99f00 Only try to print status name if it exists 2025-12-08 19:32:57 +00:00
Godfrey M
a19282710b fix up 11354 label, and readd 1d barcode to 2112283 2025-12-08 10:44:21 -08:00
Godfrey M
2f3cfb0a4e fix labelwriter fonts and add scaling 2025-12-08 10:20:53 -08:00
snipe
af4db94d17 Merge pull request #18306 from fdiaz3000/change-import-assetmodel
Change title_class to ucfirst in Object Import Command to allow AssetModel
2025-12-08 17:05:23 +00:00
snipe
bcbf27acca Merge remote-tracking branch 'origin/develop' 2025-12-08 16:51:44 +00:00
snipe
80b037c5a5 Fixed label url 2025-12-08 16:51:36 +00:00
snipe
20bacfeecf Merge remote-tracking branch 'origin/develop' 2025-12-07 21:40:19 +00:00
snipe
8a128ae8c2 Fixed RB-4062 - double-braces 2025-12-07 21:40:02 +00:00
snipe
beacfbb082 Merge remote-tracking branch 'origin/develop' 2025-12-07 15:02:17 +00:00
snipe
df0d565ae5 Set audit button back to btn-primary 2025-12-07 15:02:07 +00:00
snipe
9ee755c112 More consistent bulk action labels 2025-12-07 14:58:08 +00:00
snipe
130aca2943 Merge remote-tracking branch 'origin/develop' 2025-12-07 14:53:10 +00:00
snipe
5ea76ecb66 Fixed checkmark class on model buk edit 2025-12-07 14:52:49 +00:00
snipe
b8ff3ef41a Merge remote-tracking branch 'origin/develop' 2025-12-07 14:49:44 +00:00
snipe
3e8156be54 Merge pull request #18307 from grokability/bulk-model-edit-dark-light-fix
Fixed unreadable table on dark mode for asset model bulk edit, added breadcrumbs
2025-12-07 14:49:31 +00:00
snipe
47e192b530 Fixed breadcrumb title 2025-12-07 14:43:47 +00:00
snipe
b33f222fc0 Smaller bulk delete form 2025-12-07 14:39:00 +00:00
snipe
20eab1f403 Fixed updated route names 2025-12-07 14:37:22 +00:00
snipe
a283fdb75a Removed back button 2025-12-07 14:37:10 +00:00
snipe
a29a115846 Nicer layout on asset bulk delete form 2025-12-07 14:36:59 +00:00
snipe
05ff9183fb Fixed weird top border 2025-12-07 14:31:47 +00:00
snipe
793d299c1d Fixed breadcrumbs 2025-12-07 14:31:01 +00:00
snipe
7d5f862f34 Use striped mode 2025-12-07 14:13:17 +00:00
snipe
b0ab900a0f Fixed new dark mode for bulk model edit 2025-12-07 14:10:09 +00:00
Felix
0ea5012ba2 fix(import-command): 18305 change title_class to ucfirst 2025-12-06 15:00:43 -05:00
snipe
7ecb96d45a Merge remote-tracking branch 'origin/develop' 2025-12-06 01:08:46 +00:00
snipe
5f0d7fde39 Better selected color 2025-12-06 01:08:31 +00:00
snipe
fe3c301ca2 Prod assets 2025-12-06 00:59:55 +00:00
snipe
3adf8847b0 Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2025-12-06 00:59:47 +00:00
snipe
9ae68f0000 More color tweaks 2025-12-06 00:58:32 +00:00
snipe
36415a1f7a Updated buttons 2025-12-05 23:43:56 +00:00
snipe
39d1aa932c Moved clear button 2025-12-05 21:35:47 +00:00
snipe
82b37c3b58 Removed back button 2025-12-05 21:25:17 +00:00
snipe
c73c2b003a Merge remote-tracking branch 'origin/develop' 2025-12-05 20:52:48 +00:00
snipe
65b39d3a30 Use the themed buttons 2025-12-05 20:51:47 +00:00
snipe
6a59119c58 Merge pull request #18303 from grokability/update-users-api-with-disallowed-fields-list
Update users api with disallowed fields list
2025-12-05 20:32:55 +00:00
snipe
8b4387ec32 Fixed weird indenting 2025-12-05 19:52:31 +00:00
snipe
dc82f8f077 Updated tests 2025-12-05 19:27:05 +00:00
snipe
07e1f67e13 Disallow fill for sensitive fields 2025-12-05 19:27:01 +00:00
snipe
412f4c65c8 Fixed dark mode footer links 2025-12-05 18:35:18 +00:00
snipe
a6d9c1f882 Merge remote-tracking branch 'origin/develop' 2025-12-05 18:07:13 +00:00
snipe
bb5c142f52 Nicer footer color for light/dark 2025-12-05 18:06:48 +00:00
snipe
a5e1528c0d Merge remote-tracking branch 'origin/develop' 2025-12-05 17:46:30 +00:00
snipe
904c20e879 Removed blue text override 2025-12-05 17:46:20 +00:00
snipe
612daa6824 Merge pull request #18302 from grokability/preflight-quickstart-cleanup
Preflight quickstart cleanup
2025-12-05 17:20:29 +00:00
snipe
02b6de2385 Added tests 2025-12-05 17:03:10 +00:00
snipe
da5db1920e Use generic email address domains 2025-12-05 17:03:02 +00:00
snipe
d20545741e Nicer views 2025-12-05 17:02:51 +00:00
snipe
03b42d2c6c Nicer styles 2025-12-05 17:02:19 +00:00
snipe
e9dbeebbc4 Updated some strings 2025-12-05 17:02:09 +00:00
snipe
bb53fa245b Removed email domain from required setup fields 2025-12-05 17:01:36 +00:00
snipe
bc796498a3 Moved setup controller methods out of settings controller 2025-12-05 17:01:24 +00:00
snipe
c25266054b Merge remote-tracking branch 'origin/develop' 2025-12-05 10:55:59 +00:00
snipe
0204414196 Handle /setup link colors via middleware 2025-12-05 10:55:49 +00:00
snipe
c6b2017494 Merge pull request #18297 from Godmartinz/tze_24mm_E_2d-adjustment-and-scaling
Fixes [FD-50838] adjust `Tze_24mm_E` 2D barcode 20% bigger, scales fields, center label more
2025-12-05 08:57:20 +00:00
snipe
84fd48602e Merge pull request #18298 from Godmartinz/add-TZe_241
Adds [FD-52267] TZe_241 based on TZe_18mm sizes
2025-12-05 08:56:55 +00:00
Marcus Moore
a34ea0804d Separate out info and prompt 2025-12-04 17:22:23 -08:00
Marcus Moore
0fbf4ce443 Move singular eula to bottom of email 2025-12-04 17:08:48 -08:00
Marcus Moore
d062cc45df Add translation 2025-12-04 15:18:24 -08:00
Marcus Moore
da790136ff WIP 2025-12-04 14:40:51 -08:00
Marcus Moore
134f374ada WIP 2025-12-04 14:39:37 -08:00
Marcus Moore
df304a894f WIP 2025-12-04 14:38:13 -08:00
Marcus Moore
2d1d90e38c Add comment 2025-12-04 14:34:50 -08:00
Marcus Moore
dcbdc6fcb8 WIP 2025-12-04 14:27:53 -08:00
Marcus Moore
5a4ef15de5 Avoid rendering rule if last item in loop 2025-12-04 14:22:13 -08:00
Marcus Moore
affc4c8bd9 Styling 2025-12-04 14:07:10 -08:00
Marcus Moore
bc5d6e89ba Readability 2025-12-04 14:05:22 -08:00
Marcus Moore
c17e6811d2 Group categories visually 2025-12-04 14:03:11 -08:00
Marcus Moore
7f097c029a Fix indent 2025-12-04 13:52:26 -08:00
Godfrey M
c8e8eb58aa adds TZe_241 based on TZe_18mm sizes 2025-12-04 12:41:57 -08:00
akemidx
60f5affe43 more sharing framewor. tests outlines 2025-12-04 13:48:36 -05:00
Godfrey M
0b087ca77d adjust 2d barcode 20% bigger, scale fields, center label more 2025-12-04 09:55:47 -08:00
snipe
444083ec5d Merge remote-tracking branch 'origin/develop' 2025-12-04 17:54:31 +00:00
snipe
bf01a11fec Fixed label colors in dark/light 2025-12-04 17:54:19 +00:00
snipe
8f232421d2 Merge remote-tracking branch 'origin/develop' 2025-12-04 17:37:58 +00:00
snipe
dbc688ad6e Set '#ffffff' as default 2025-12-04 17:37:44 +00:00
snipe
6217a721ac Merge remote-tracking branch 'origin/develop' 2025-12-04 15:11:38 +00:00
snipe
c2ba937ac6 Merge pull request #18295 from grokability/#18288-allow-reference-editing-if-edit-profile-is-disabled
Fixed #18288: Allow users to edit display preferences even if profile editing is not enabled
2025-12-04 15:11:11 +00:00
snipe
d860786221 Re-add button 2025-12-04 13:21:54 +00:00
snipe
621ce1777f Fixed #18288: Allow users to change preferences even if profile editing is not permitted 2025-12-04 13:21:04 +00:00
snipe
4f610ac1af Added tag color to importer 2025-12-04 11:07:59 +00:00
snipe
7341cd1712 Merge pull request #18294 from grokability/#18278-import-company-in-locations
Fixed #18278: Import companies into locations on initial import
2025-12-04 11:07:18 +00:00
snipe
bf112b7b4b Fixed #18278: Import companies into locations on initial import 2025-12-04 10:59:45 +00:00
snipe
ad9e0cc39a Merge remote-tracking branch 'origin/develop' 2025-12-04 10:50:09 +00:00
snipe
1439681113 Alert message link text 2025-12-04 10:34:58 +00:00
snipe
3b750541c9 Prod assets 2025-12-04 10:19:12 +00:00
snipe
79765201ac Merge remote-tracking branch 'origin/develop' 2025-12-04 10:18:37 +00:00
snipe
0086b9d848 mix 2025-12-04 10:17:17 +00:00
snipe
c8ddb44783 Fixed button color 2025-12-04 10:16:44 +00:00
snipe
d4829a4bac Fixed #18286 - :user in declined email 2025-12-04 10:16:35 +00:00
snipe
486f0c0035 Re-running assets for prod 2025-12-04 08:46:53 +00:00
snipe
dc3a695ab0 Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	config/version.php
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/css/dist/skins/_all-skins.css
#	public/css/dist/skins/_all-skins.css.map
#	public/css/dist/skins/_all-skins.min.css
#	public/css/dist/skins/skin-black-dark.css
#	public/css/dist/skins/skin-black-dark.css.map
#	public/css/dist/skins/skin-black-dark.min.css
#	public/css/dist/skins/skin-black.css
#	public/css/dist/skins/skin-black.css.map
#	public/css/dist/skins/skin-black.min.css
#	public/css/dist/skins/skin-blue-dark.css
#	public/css/dist/skins/skin-blue-dark.css.map
#	public/css/dist/skins/skin-blue-dark.min.css
#	public/css/dist/skins/skin-blue.css
#	public/css/dist/skins/skin-blue.css.map
#	public/css/dist/skins/skin-blue.min.css
#	public/css/dist/skins/skin-contrast.css
#	public/css/dist/skins/skin-contrast.css.map
#	public/css/dist/skins/skin-contrast.min.css
#	public/css/dist/skins/skin-green-dark.css
#	public/css/dist/skins/skin-green-dark.css.map
#	public/css/dist/skins/skin-green-dark.min.css
#	public/css/dist/skins/skin-green.css
#	public/css/dist/skins/skin-green.css.map
#	public/css/dist/skins/skin-green.min.css
#	public/css/dist/skins/skin-orange-dark.css
#	public/css/dist/skins/skin-orange-dark.css.map
#	public/css/dist/skins/skin-orange-dark.min.css
#	public/css/dist/skins/skin-orange.css
#	public/css/dist/skins/skin-orange.css.map
#	public/css/dist/skins/skin-orange.min.css
#	public/css/dist/skins/skin-purple-dark.css
#	public/css/dist/skins/skin-purple-dark.css.map
#	public/css/dist/skins/skin-purple-dark.min.css
#	public/css/dist/skins/skin-purple.css
#	public/css/dist/skins/skin-purple.css.map
#	public/css/dist/skins/skin-purple.min.css
#	public/css/dist/skins/skin-red-dark.css
#	public/css/dist/skins/skin-red-dark.css.map
#	public/css/dist/skins/skin-red-dark.min.css
#	public/css/dist/skins/skin-red.css
#	public/css/dist/skins/skin-red.css.map
#	public/css/dist/skins/skin-red.min.css
#	public/css/dist/skins/skin-yellow-dark.css
#	public/css/dist/skins/skin-yellow-dark.css.map
#	public/css/dist/skins/skin-yellow-dark.min.css
#	public/css/dist/skins/skin-yellow.css
#	public/css/dist/skins/skin-yellow.css.map
#	public/css/dist/skins/skin-yellow.min.css
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2025-12-04 08:45:19 +00:00
akemidx
e769239213 only user can change report sharing 2025-12-04 00:39:47 -05:00
akemidx
45923a74f6 only user can change report sharing 2025-12-04 00:34:30 -05:00
Marcus Moore
7bf7a87f8a Begin to display EULAs for all categories 2025-12-03 16:59:57 -08:00
Marcus Moore
8396e27a2c Revert "Avoid showing EULA"
This reverts commit 8c89eb6650.
2025-12-03 16:16:30 -08:00
Marcus Moore
5153c68b8b Remove old todos 2025-12-03 16:15:43 -08:00
snipe
3feee682b6 Fixed import view with light/dark 2025-12-03 21:38:57 +00:00
snipe
4ea5cb9538 Merge pull request #18284 from Godmartinz/fix-alert-threshold-bug
Fixes Low Inventory Alerts not sending when threshold is `null`
2025-12-03 21:32:29 +00:00
Godfrey M
f6461a755a revert again 2025-12-03 13:22:46 -08:00
Godfrey M
39cf5ce66e revert some changes 2025-12-03 13:22:03 -08:00
Godfrey M
c68d9892b5 fix threshold bug, include minimum to available 2025-12-03 13:20:01 -08:00
snipe
33b20b6268 Fixed sidemenu hover 2025-12-03 19:27:44 +00:00
snipe
280df20a0b Better default link colors for skin migration 2025-12-03 18:18:24 +00:00
snipe
7027cd80d4 Updated modal for light/dark 2025-12-03 18:18:14 +00:00
snipe
bfbcfe7bae Updated text string 2025-12-03 17:36:48 +00:00
snipe
e1f64b6d2b Modal title color fix 2025-12-03 17:36:39 +00:00
snipe
6944c438dd Inline tooltip improvements 2025-12-03 16:04:23 +00:00
snipe
49b7ff1192 Fixed email field color 2025-12-03 15:58:50 +00:00
snipe
662cdbaa0e Datepicker color fixes 2025-12-03 15:55:04 +00:00
snipe
e912eb5ef8 Merge pull request #18279 from uberbrady/new_allowlist_restore_cleaner
Loosen regex allowlist for setting character sets
2025-12-03 13:31:01 +00:00
Brady Wetherington
5ab68d83a5 Loosen regex allowlist for setting character sets 2025-12-03 13:06:02 +00:00
snipe
f7c432f7fd Bumped hash 2025-12-03 10:09:23 +00:00
snipe
592ccb6ebe Merge pull request #18244 from Godmartinz/receive-all-expired-licenses-on-report
adds #17422 [FD-49345] `--expired-licenses` command parameter to `snipeit:expiring-alerts`
2025-12-03 09:49:49 +00:00
snipe
d22d70dd92 Merge pull request #18257 from iryadifarhan/fix/manager-view-not-displaying-subordinates-eulas-properly
Fixes Manager View not displaying subordinates EULAs properly in View Assets page
2025-12-03 09:49:35 +00:00
snipe
7ec5606ce4 Merge pull request #18247 from uberbrady/multi_create_fixes
Fixed #18160 - Multi-create fixes
2025-12-03 09:49:14 +00:00
snipe
476bf95edf Merge pull request #18272 from spencerrlongg/add-null-checks
Adds Null Checks
2025-12-03 09:48:43 +00:00
Marcus Moore
391495dd86 Remove some assertions 2025-12-02 17:41:22 -08:00
Marcus Moore
8c89eb6650 Avoid showing EULA 2025-12-02 17:37:21 -08:00
Marcus Moore
4167c6ea70 Add some translations 2025-12-02 17:36:05 -08:00
Marcus Moore
ca3151ce29 Improve naming 2025-12-02 16:10:53 -08:00
akemidx
cc20844eff share template bits 2025-12-02 18:34:13 -05:00
Marcus Moore
d876e710e4 Be more specific in tests 2025-12-02 13:48:59 -08:00
Marcus Moore
5c1290425b Improve variable name 2025-12-02 13:28:59 -08:00
Marcus Moore
2043488c67 Cleanups 2025-12-02 12:31:04 -08:00
Marcus Moore
e7e48c8f03 Cleanups 2025-12-02 12:30:16 -08:00
Marcus Moore
dad650b804 Readability 2025-12-02 12:28:51 -08:00
Marcus Moore
d0e73714c6 Implement test 2025-12-02 11:57:02 -08:00
Marcus Moore
d8b95d3a20 Organization 2025-12-02 11:53:37 -08:00
Marcus Moore
559d8cc0db Implement test 2025-12-02 11:53:01 -08:00
Marcus Moore
7a804aa576 Implement test 2025-12-02 11:51:28 -08:00
snipe
cdf036ed7b Merge pull request #18274 from spencerrlongg/check-that-email-exists-on-recipient
Check That Email Exists on Recipient in Checkout Acceptance
2025-12-02 19:35:28 +00:00
snipe
639a3b9295 Merge pull request #18273 from Godmartinz/fix-null-assignee-bug-in-checkoutable
Refactor `assignee` in Checkoutable to accept null
2025-12-02 19:31:22 +00:00
spencerrlongg
e4f8c3bef7 add null check and check for email 2025-12-02 13:17:13 -06:00
Godfrey M
462945022c allow null for assignee in checkoutable 2025-12-02 11:05:07 -08:00
spencerrlongg
aa57687df0 add null checks to license 2025-12-02 12:13:38 -06:00
snipe
3237a3b9de One more button fix 2025-12-02 16:44:31 +00:00
snipe
ff30e109cc Small button fixes 2025-12-02 16:42:32 +00:00
snipe
2d291f843a Fixed create new on table buttons 2025-12-02 16:36:54 +00:00
snipe
7219fc1c3c Added style to border on colorpicker 2025-12-02 16:22:53 +00:00
snipe
ed6bfa7810 Disable branding colopickers on demos 2025-12-02 16:16:33 +00:00
snipe
ad15090c34 Nicer search highlight box 2025-12-02 15:58:34 +00:00
snipe
9d34bf4a19 Fixed table header colors 2025-12-02 15:54:18 +00:00
snipe
7c9b1a52af Swapped link colors 2025-12-02 15:39:32 +00:00
snipe
dd297dca31 Merge pull request #18249 from grokability/proper-dark-toggle
Experiment: simpler light/dark toggle
2025-12-02 14:37:52 +00:00
snipe
1409d01078 Tweaked color 2025-12-02 14:19:53 +00:00
snipe
c9a03cf9b7 Final color tweaks 2025-12-02 14:03:08 +00:00
snipe
6a99132e76 More tweaks 2025-12-02 13:29:12 +00:00
Marcus Moore
0bca66b671 Send email if asset has checkin_email set to true 2025-12-01 16:58:02 -08:00
Marcus Moore
24e5cf8121 Improve readability 2025-12-01 16:51:09 -08:00
Marcus Moore
ee7c4ce0f3 Improve assertion 2025-12-01 16:47:23 -08:00
Marcus Moore
428b511687 Send if eula is set 2025-12-01 16:41:50 -08:00
Marcus Moore
49497996d5 Fix template 2025-12-01 16:41:39 -08:00
Marcus Moore
bccd65e2fc Add failing test 2025-12-01 16:33:46 -08:00
Marcus Moore
f2158843ce Avoid attempting to loop over null 2025-12-01 16:25:37 -08:00
Marcus Moore
87fc4a4f22 Scaffold scenarios 2025-12-01 16:01:34 -08:00
Marcus Moore
27291f9ee9 Add todo 2025-12-01 14:23:33 -08:00
Marcus Moore
cba963110e Remove unused import 2025-12-01 14:22:06 -08:00
Marcus Moore
aa014e3706 Improve wording 2025-12-01 14:19:54 -08:00
akemidx
58d577f67a shared/notshared marker 2025-12-01 16:48:39 -05:00
Marcus Moore
cd3678841b Fix intro line to locations 2025-12-01 13:41:23 -08:00
Marcus Moore
425e0c33df Add tests for introduction line 2025-12-01 12:55:39 -08:00
Marcus Moore
2018407782 Avoid error by pre-checking if user has email address 2025-12-01 12:04:53 -08:00
snipe
2e269d2e63 Removed old skin less files 2025-11-29 10:42:10 +00:00
snipe
7820636c9f Nicer defaults 2025-11-29 10:41:58 +00:00
snipe
db1b35ccf6 Fixed style 2025-11-28 19:20:41 +00:00
snipe
fadfe0a782 Removed old skin references 2025-11-28 19:17:52 +00:00
snipe
255a2ecdd9 Sigh 2025-11-28 19:09:38 +00:00
snipe
97ffe33fc8 Check that a setting record exists 2025-11-28 19:07:46 +00:00
snipe
56d97a1f59 Updated map 2025-11-28 19:05:25 +00:00
snipe
28d5d24617 Migration to handle skins 2025-11-28 19:05:19 +00:00
snipe
d97f6903d6 Save settings controller 2025-11-28 19:05:03 +00:00
snipe
3bf84d96d9 Update language 2025-11-28 19:04:49 +00:00
snipe
8df643a2ab Removed user skin option 2025-11-28 19:04:40 +00:00
snipe
2d001c4fa1 Added colorpickers for link colors 2025-11-28 19:04:20 +00:00
snipe
cbd6b57445 Removed skin from user profile update 2025-11-28 19:04:00 +00:00
snipe
dac684c08a Update demo settings 2025-11-28 19:03:48 +00:00
snipe
772c29791a Use css variable 2025-11-28 17:48:08 +00:00
snipe
89a232ae14 Merge pull request #18266 from Valinwolf/develop
Added endpoint & use_path_style_endpoint configs for public/private S3
2025-11-28 17:39:23 +00:00
snipe
4e4b8ddb77 And more updates 2025-11-28 17:33:30 +00:00
Patrick Thomas
6eaefa0bdd Added endpoint & use_path_style_endpoint configs for public/private S3 2025-11-28 17:29:02 +00:00
snipe
20a75bbbb7 More styles 2025-11-28 15:54:24 +00:00
snipe
5cc261dd3c Smaller LDAP screen 2025-11-28 15:50:37 +00:00
snipe
6d958b6f65 Added fa-fw to arrow class 2025-11-28 15:50:25 +00:00
snipe
8ddac4d7c7 More select2 styling :( 2025-11-28 15:16:22 +00:00
snipe
a321ad9dbe Handle select2 stuff 2025-11-28 14:13:19 +00:00
snipe
4dff66253c Added contrast-color to dynamically pick white/black for topnav 2025-11-27 16:15:35 +00:00
snipe
9a1e9f90bc Better preview for header color, updated text 2025-11-27 15:56:06 +00:00
snipe
c54724919c Show header color preview 2025-11-27 15:29:24 +00:00
snipe
139d1cdcf8 Added a few more classes 2025-11-27 14:48:28 +00:00
snipe
490c50a182 Added fa-fw to action buttons 2025-11-27 14:48:16 +00:00
snipe
af1e496eab Added correct box class 2025-11-27 13:30:11 +00:00
snipe
efea043549 Added dark/light text 2025-11-27 13:29:59 +00:00
snipe
d4ee91f013 Removed a few classes 2025-11-27 13:29:50 +00:00
iryadifarhan
d4561581ad Apply fix around view-assets to pass request parameter and profile controller to address request parameter 2025-11-27 13:42:47 +07:00
snipe
a17f167952 Fix button overrides 2025-11-26 15:41:38 +00:00
snipe
5beb068cde More updates for dark and light switches 2025-11-26 15:35:43 +00:00
snipe
a272bdc796 Merge pull request #18251 from uberbrady/improve_component_asset_counts
Optimize queries for Components listing
2025-11-26 14:40:14 +00:00
snipe
30a43089a0 More variablization 2025-11-26 13:50:39 +00:00
Brady Wetherington
416b32cbc8 Optimize queries for Components listing 2025-11-26 12:36:44 +00:00
iryadifarhan
a3a49e47b7 Apply toDateString so that the it equally compare date only, evades including time/hour comparing 2025-11-26 13:21:15 +07:00
snipe
d203cece0e Removed indicidual skins 2025-11-26 04:08:31 +00:00
snipe
9f6f0f04c7 Few more tweaks 2025-11-26 04:06:20 +00:00
iryadifarhan
a4729a7de8 Fix implemented by using existing function from Depreciable trait, hence addressing the floor value 2025-11-26 10:17:21 +07:00
snipe
a974c6d4cd Moved icon-med 2025-11-26 02:56:05 +00:00
snipe
34612acdcf Experiment: light/dark simplifcation 2025-11-26 02:49:40 +00:00
snipe
9e23117f9c Merge remote-tracking branch 'origin/develop' 2025-11-26 00:35:26 +00:00
snipe
b3996f1970 In the sea, @uberbrady! (fixed missing semicolon) 2025-11-26 00:35:16 +00:00
snipe
e143017432 Tinkering with CSS/JS dark more 2025-11-26 00:34:46 +00:00
Brady Wetherington
c6c0a14ee0 Whoops, used PHP's equal signs instead of MySQL's :/ 2025-11-25 20:23:15 +00:00
Brady Wetherington
9b8768dbdd Tighten everything up, remove logging, fix last bits of functionality 2025-11-25 19:34:10 +00:00
snipe
a3bfcc962d Merge remote-tracking branch 'origin/develop' 2025-11-25 18:50:23 +00:00
snipe
ca4ed605a8 Merge pull request #18246 from Godmartinz/resize-label-fields-conditionally-L6009_A
Fixes [FD-52064] Avery `L6009_A` & `L4736_A` label field overflow with scaling
2025-11-25 18:37:06 +00:00
Godfrey M
d3e6d7442f typo bonanza 2025-11-25 10:30:56 -08:00
Godfrey M
b558bc5334 change variable" 2025-11-25 10:30:06 -08:00
Godfrey M
204d7b5be6 added scaling to L4736_A 2025-11-25 10:23:08 -08:00
Godfrey M
7dccfec332 adds notes 2025-11-25 10:12:27 -08:00
snipe
0b694bfd0b Merge remote-tracking branch 'origin/develop' 2025-11-25 18:02:23 +00:00
snipe
dfb59d8a55 Added link to rudder2snipe repo 2025-11-25 18:01:29 +00:00
Brady Wetherington
3cd191210c Merge branch 'develop' into multi_create_fixes 2025-11-25 13:58:56 +00:00
snipe
56a44ad421 Merge remote-tracking branch 'origin/develop' 2025-11-25 13:38:54 +00:00
snipe
a12ee3c0da Merge pull request #18245 from uberbrady/redirect_upgrader
Add new 'git remote' management to change Snipe-IT URLs
2025-11-25 13:38:31 +00:00
Brady Wetherington
a657c479be Add new 'git remote' management to change Snipe-IT URL's 2025-11-25 13:17:56 +00:00
Godfrey M
bb7dabc73b adds expired-licenses command parameter 2025-11-24 11:41:50 -08:00
Godfrey M
ab82c5fd88 resizes label field box to size if needed 2025-11-24 11:04:23 -08:00
snipe
78cfb19f69 Merge remote-tracking branch 'origin/develop' 2025-11-24 17:40:26 +00:00
snipe
f2334082ee Added tag color to location query 2025-11-24 17:40:13 +00:00
snipe
58a47cb52b Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	config/version.php
2025-11-24 12:32:17 +00:00
snipe
c7cb4674f5 Bumped version 2025-11-24 12:31:50 +00:00
snipe
523df21d83 Merge remote-tracking branch 'origin/develop' 2025-11-24 12:30:22 +00:00
snipe
82314076a9 Updated translations 2025-11-24 12:30:07 +00:00
snipe
d04bf2e8f2 Merge remote-tracking branch 'origin/develop' 2025-11-24 11:37:44 +00:00
snipe
a1c67b5154 Fixed user details toggle 2025-11-24 11:37:14 +00:00
snipe
3e343fe8b7 Merge pull request #18236 from grokability/dependabot/github_actions/develop/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-11-24 08:33:46 +00:00
dependabot[bot]
e288c942ee Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 08:04:19 +00:00
snipe
7a1ccb0d53 Nicer query 2025-11-23 14:09:55 +00:00
snipe
89656c7e65 Merge remote-tracking branch 'origin/develop' 2025-11-23 14:05:39 +00:00
snipe
7f76198139 Fixed RB-20501 - correctly return error response when license+seat don’t match
TypeError: App\Http\Transformers\LicenseSeatsTransformer::transformLicenseSeat(): Argument #1 ($seat) must be of type App\Models\LicenseSeat, bool given, called in /snipe-it/app/Http/Controllers/Api/LicenseSeatsController.php on line 92
2025-11-23 14:05:24 +00:00
snipe
5ff813f9b7 Merge pull request #18233 from grokability/develop
Merging from dev
2025-11-22 14:24:36 +00:00
snipe
69994e0c11 Merge pull request #18232 from grokability/check-for-valid-json-on-filter
Added filter form request to validate JSON
2025-11-22 14:22:33 +00:00
snipe
aa959fbe92 Added filter form request to validate JSON 2025-11-22 14:03:58 +00:00
snipe
948b7cda15 Merge remote-tracking branch 'origin/develop' 2025-11-22 12:04:28 +00:00
snipe
644ef040d0 Merge pull request #18226 from Godmartinz/audit-notification-rb-19608
Fixes RB-19608 adds safe guards, adds Audit notification for google workspace, adds tests
2025-11-22 11:41:57 +00:00
snipe
33839d7244 Re-added the horizontal class 2025-11-22 11:14:59 +00:00
Godfrey M
48270cb9b4 url trim 2025-11-21 10:11:28 -08:00
Godfrey M
cd42760b68 notes 2025-11-20 14:25:13 -08:00
Godfrey M
1116da389e add google chat to audit notification test 2025-11-20 14:24:03 -08:00
Godfrey M
24d7ae4a2f added google chat to audit notifications" 2025-11-20 14:22:08 -08:00
Godfrey M
e598ef6e05 adds tests, webhook enable settings 2025-11-20 13:51:14 -08:00
snipe
6601e73069 Merge remote-tracking branch 'origin/develop' 2025-11-20 20:15:44 +00:00
snipe
7f2a80552b Fixed form layout for SAML 2025-11-20 20:15:34 +00:00
Godfrey M
eceeb4aa3b return if there is no item for the audit notification 2025-11-20 10:56:43 -08:00
snipe
b5c7f374f3 Merge remote-tracking branch 'origin/develop' 2025-11-20 13:44:19 +00:00
snipe
9d08e2d297 Clearer language 2025-11-20 13:44:09 +00:00
snipe
f2303ae2dc Merge remote-tracking branch 'origin/develop' 2025-11-20 13:28:00 +00:00
snipe
4d44fd47c3 Merge pull request #18217 from Godmartinz/unaccepted-row-null-fix
Fixes FD-52005 Adds null safe operators to unacceptable items report
2025-11-20 13:27:24 +00:00
snipe
88635cb6c3 Merge pull request #18223 from grokability/#18114-fixed-declined-notification-added-x-header
Fixed #18114 - include declined item name, added app headers
2025-11-20 13:27:01 +00:00
snipe
1687fcc035 Small subject tweaks 2025-11-20 13:23:17 +00:00
snipe
39e7937458 Fixed #18114 - include declined item name, added app headers 2025-11-20 12:58:53 +00:00
Marcus Moore
54f065f42c Improve test 2025-11-19 17:11:39 -08:00
Marcus Moore
333ebb88b9 Enable sending to manager 2025-11-19 17:08:00 -08:00
Marcus Moore
53ff367473 Add failing tests 2025-11-19 16:31:48 -08:00
Marcus Moore
33a7de9448 Add custom fields to email 2025-11-19 13:31:30 -08:00
Godfrey M
60ff2970f0 safeguard null rows by filtering rows in query 2025-11-19 11:35:24 -08:00
Godfrey M
212cd026a3 add null safe operators to company and csv plain values 2025-11-18 12:36:07 -08:00
snipe
cf47f8fea9 Merge pull request #18216 from Godmartinz/fix-license-patch-to-allow-update-of-other-fields
Fixed #18024 License Seat update/patch method
2025-11-18 20:29:18 +00:00
akemidx
c797472bcc migration, and front end 2025-11-18 15:03:28 -05:00
Godfrey M
4b45ffd841 updating fields of checkout now works 2025-11-18 11:35:26 -08:00
snipe
69a57b77c9 Merge remote-tracking branch 'origin/develop' 2025-11-18 13:13:48 +00:00
snipe
5daba6034d Merge pull request #18213 from grokability/#18202-copy-to-clipboard-spaces
Fixed #18202 - copy to clipboard adding spaces in FF
2025-11-18 13:13:29 +00:00
snipe
4e5c19e932 Fixed #18202 - copy to clipboard adding spaces in FF 2025-11-18 13:11:40 +00:00
snipe
5eb73baf5e Removed duplicate formatter 2025-11-18 12:54:21 +00:00
snipe
d819f31bdd Merge remote-tracking branch 'origin/develop' 2025-11-18 12:01:01 +00:00
snipe
a97c453706 Small fixes for user profiles 2025-11-18 11:59:54 +00:00
snipe
5ee955a713 Merge remote-tracking branch 'origin/develop' 2025-11-18 11:40:23 +00:00
snipe
57224f7304 Fixed #18209 - translate asset filters 2025-11-18 11:40:15 +00:00
snipe
270d145466 Merge remote-tracking branch 'origin/develop' 2025-11-18 11:15:31 +00:00
snipe
c564ee6093 Fixed #18211 - limit regex field to 191 characters 2025-11-18 11:15:15 +00:00
snipe
caaa9ab23e Merge pull request #18122 from chruoss/master
Renamed L6009 -> L4736 and added correct L6009
2025-11-17 21:40:08 +00:00
snipe
b6d2c6d28c Merge pull request #18208 from marcusmoore/fixes/null-array-filter
Fixed potential exception while filtering in users index endpoint
2025-11-17 21:39:08 +00:00
Marcus Moore
c36c8968d3 Avoid passing null to array_filter in user controller 2025-11-17 10:26:29 -08:00
snipe
b019af1851 Merge remote-tracking branch 'origin/develop' 2025-11-17 17:50:30 +00:00
snipe
bcd32da2bc Remove loading user count since we don’t use it for depts 2025-11-17 17:34:43 +00:00
snipe
6f97a40372 Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/css/build/app.css
#	public/css/build/app.css.map
#	public/css/build/overrides.css
#	public/css/build/overrides.css.map
#	public/css/dist/all.css
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2025-11-17 13:37:16 +00:00
snipe
c56c7ddd03 Merge pull request #18201 from grokability/#18185-added-tag-color-to-companies
Added #18185 added tag color to companies
2025-11-17 13:33:17 +00:00
snipe
9a0dd604c9 Small layout tweaks 2025-11-17 13:21:01 +00:00
snipe
a1dec176a1 Added mobile 2025-11-17 13:09:13 +00:00
snipe
67032d068d Removed space 2025-11-17 13:09:08 +00:00
snipe
401c83945d Updated gate check on presenters 2025-11-17 13:07:28 +00:00
snipe
ed6d020edb Fixed permission name for locations 2025-11-17 12:31:18 +00:00
snipe
8eb5600b1e Fixed manufacturer link 2025-11-17 12:19:27 +00:00
snipe
9c4cd69106 Updated checkout blades 2025-11-17 12:15:38 +00:00
snipe
64cbe0c960 Use dept presenter 2025-11-17 12:15:25 +00:00
snipe
be70b217d9 Updated presenters 2025-11-17 12:11:38 +00:00
snipe
b4f5260dda Updated controllers to use tag_color 2025-11-17 11:36:11 +00:00
snipe
83e446f99c Updated formatters to use tag_color if given 2025-11-17 11:18:38 +00:00
snipe
767139cf0b Updated select2 to use tag_color if available 2025-11-17 11:18:21 +00:00
snipe
e3ac60111f Added tag_color to factories 2025-11-17 11:18:02 +00:00
snipe
c694c11724 Added presenter reference 2025-11-15 14:58:02 +00:00
snipe
b50765a151 Added more to the views 2025-11-15 14:50:51 +00:00
snipe
590f77bdb4 Updated controllers and presenters 2025-11-14 17:58:27 +00:00
snipe
09575e5312 Added tag color to transformer 2025-11-14 15:58:07 +00:00
snipe
1693825dd0 Added translations 2025-11-14 15:57:57 +00:00
snipe
5a89056112 Added migration 2025-11-14 15:57:47 +00:00
snipe
2ee51bb282 Added tag color to transformer 2025-11-14 15:57:39 +00:00
snipe
ea7cffc1a3 Made tag_color fillable 2025-11-14 15:57:27 +00:00
snipe
c20d1b82ae Added tag color to controllers 2025-11-14 15:57:18 +00:00
snipe
607781382f Merge remote-tracking branch 'origin/develop' 2025-11-14 14:10:05 +00:00
snipe
4e8c1e853b Fixed markdown in upcoming audits email 2025-11-14 14:09:53 +00:00
snipe
2c6869501e Merge remote-tracking branch 'origin/develop' 2025-11-13 16:46:32 +00:00
snipe
4509e1d1dc Merge pull request #18195 from grokability/#18189-day-of-week
Fixed #18189 - added option to pick the day the week starts on
2025-11-13 16:46:15 +00:00
snipe
6b9d4941be Language tweaks 2025-11-13 16:35:40 +00:00
snipe
308cd6b91d Fixed #18189 - added option to pick the day the week starts on 2025-11-13 16:33:08 +00:00
snipe
23b54de8bd Merge remote-tracking branch 'origin/develop' 2025-11-13 15:37:42 +00:00
snipe
bd742aec9c Merge pull request #18192 from Godmartinz/fix-requestable-asset-model-query
Fixes FD51910 requestable asset model available quantity count
2025-11-13 15:37:24 +00:00
snipe
d2ab3071a6 Merge pull request #18193 from ubc-cpsc/bugfix/CVE-2025-64500
Fixes CVE-2025-64500: Incorrect parsing of PATH_INFO can lead to limited authorization
2025-11-13 09:52:50 +00:00
Joël Pittet
1dd4c161f0 Fixes CVE-2025-64500 2025-11-12 18:09:37 -08:00
Godfrey M
5e48dd45b2 remove unnecessary code 2025-11-12 16:27:04 -08:00
Godfrey M
601d6e7377 fixes requestable models query 2025-11-12 16:25:08 -08:00
snipe
3934b40282 Merge remote-tracking branch 'origin/develop' 2025-11-12 20:59:15 +00:00
snipe
d43be271e6 Small tweaks to user group listings 2025-11-12 20:59:06 +00:00
snipe
d3c9963051 Merge remote-tracking branch 'origin/develop' 2025-11-12 20:25:00 +00:00
snipe
92a3bdf4e9 Merge pull request #18191 from grokability/limit-adding-users-to-group-if-over-limit
Set a limit on number of users for group user loading
2025-11-12 20:24:24 +00:00
snipe
fa2aafe41f Set a limit on number of users for group user loading 2025-11-12 20:19:52 +00:00
snipe
bc5da2532c Merge remote-tracking branch 'origin/develop' 2025-11-12 16:02:01 +00:00
snipe
a7be1acbd8 Merge pull request #18190 from grokability/improved-checkin-reminders
Improved overdue checkin alert
2025-11-12 16:01:32 +00:00
snipe
fbe2ae03ff Use due_checkin_days setting instead of audit warning days 2025-11-12 15:52:16 +00:00
snipe
d1a492f953 Improved overdue checkin alert 2025-11-12 15:38:47 +00:00
snipe
ac6ea8bdcc Merge remote-tracking branch 'origin/develop' 2025-11-11 18:43:54 +00:00
snipe
352807c2d7 Fixed RB-20498 - Check that logo file exists on acceptance 2025-11-11 18:43:39 +00:00
Brady Wetherington
70d79c1890 Merge branch 'develop' into multi_create_fixes 2025-11-11 18:20:49 +00:00
Brady Wetherington
fb1fde26ce Use a FormRequest to better handle multiple-asset-creation by GUI 2025-11-11 18:00:22 +00:00
snipe
dc4cf8496a Merge remote-tracking branch 'origin/develop' 2025-11-11 14:01:54 +00:00
snipe
3aa046bfa7 Include trashed in other acceptance tasks 2025-11-11 14:00:48 +00:00
snipe
ed79d21e1b Merge remote-tracking branch 'origin/develop' 2025-11-11 13:53:55 +00:00
snipe
9454ff677b Get deleted objects in unaccepted asset report 2025-11-11 13:53:43 +00:00
snipe
743d340bca Merge remote-tracking branch 'origin/develop' 2025-11-11 13:18:18 +00:00
snipe
a5c7b8f609 Small unaccepted items report fixes 2025-11-11 13:17:56 +00:00
snipe
3d1398ab97 Merge remote-tracking branch 'origin/develop' 2025-11-11 12:29:44 +00:00
snipe
9365d1adc6 Merge pull request #18164 from Godmartinz/fix-accessories-redirect-previous-page
Fixes #18138 redirect to Previous Page from accessories edit screen
2025-11-11 12:29:23 +00:00
snipe
ddd2a96ac5 Clearer message on no inventory to report 2025-11-11 11:52:05 +00:00
Godfrey M
6faf171007 update asset_deployable translation 2025-11-10 16:58:42 -08:00
snipe
7a680ce4ff Merge remote-tracking branch 'origin/develop' 2025-11-10 21:31:28 +00:00
snipe
bf8ff51234 Make suppliers notes nullable by default 2025-11-10 21:31:14 +00:00
snipe
2ac4456f4e Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	config/version.php
2025-11-10 20:58:15 +00:00
snipe
7891f52bd4 Merge pull request #18177 from grokability/add-files-to-suppliers
Fixed #12451: Add file upload support to suppliers, changed notes field to text (from varchar)
2025-11-10 20:57:21 +00:00
snipe
baa4a8a461 Migration to change notes on suppliers to text from varchar 2025-11-10 20:53:21 +00:00
snipe
34daffcdf4 Added supplier file uploads 2025-11-10 20:47:45 +00:00
snipe
21baea27a8 Merge pull request #18141 from uberbrady/actiontype_enum_redux
Actiontype enum redux
2025-11-10 18:55:01 +00:00
snipe
e1d3714445 Add @MarvelousAnything as a contributor 2025-11-10 14:15:10 +00:00
snipe
8b7e0a0d78 Add @mohammad-ahmadi1 as a contributor 2025-11-10 14:15:01 +00:00
snipe
a1cc427c9c Add @smarsching as a contributor 2025-11-10 14:14:51 +00:00
snipe
391aa30da2 Bumped version 2025-11-10 13:49:26 +00:00
snipe
5dc675040d Bumped version 2025-11-10 13:48:59 +00:00
snipe
1258eb6533 Merge pull request #17913 from Godmartinz/add_types_to_unaccepted_asset_report
Fixed #15664 - Adds Accessories, Components, Consumables, and Licenses to Unaccepted Assets report
2025-11-10 12:24:13 +00:00
snipe
f92f76b48a Merge remote-tracking branch 'origin/develop' 2025-11-07 13:42:04 +00:00
snipe
83abfc9ca6 Updated language files 2025-11-07 13:41:46 +00:00
snipe
61b6d4dc47 Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/css/dist/skins/_all-skins.css
#	public/css/dist/skins/_all-skins.css.map
#	public/css/dist/skins/_all-skins.min.css
#	public/css/dist/skins/skin-black-dark.css
#	public/css/dist/skins/skin-black-dark.css.map
#	public/css/dist/skins/skin-black-dark.min.css
#	public/css/dist/skins/skin-blue-dark.css
#	public/css/dist/skins/skin-blue-dark.css.map
#	public/css/dist/skins/skin-blue-dark.min.css
#	public/css/dist/skins/skin-green-dark.css
#	public/css/dist/skins/skin-green-dark.css.map
#	public/css/dist/skins/skin-green-dark.min.css
#	public/css/dist/skins/skin-orange-dark.css
#	public/css/dist/skins/skin-orange-dark.css.map
#	public/css/dist/skins/skin-orange-dark.min.css
#	public/css/dist/skins/skin-purple-dark.css
#	public/css/dist/skins/skin-purple-dark.css.map
#	public/css/dist/skins/skin-purple-dark.min.css
#	public/css/dist/skins/skin-red-dark.css
#	public/css/dist/skins/skin-red-dark.css.map
#	public/css/dist/skins/skin-red-dark.min.css
#	public/css/dist/skins/skin-yellow-dark.css
#	public/css/dist/skins/skin-yellow-dark.css.map
#	public/css/dist/skins/skin-yellow-dark.min.css
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2025-11-07 13:26:23 +00:00
snipe
b42d6677cc Updated assets 2025-11-07 13:25:45 +00:00
snipe
e8c644a600 Merge pull request #18165 from Godmartinz/border-color-create-new-button-fix
Fixes #18140 Changes border color of create New in Dark modes
2025-11-07 13:21:52 +00:00
snipe
aa041e39eb Merge remote-tracking branch 'origin/develop'
# Conflicts:
#	public/js/dist/all.js
#	public/js/dist/all.js.map
#	public/mix-manifest.json
2025-11-06 21:12:42 +00:00
snipe
9f6a73b290 Merge pull request #18170 from grokability/fix-for-groups
Fixed #18157 - only partial information stored on group save if lower `max_input_vars` and/or `max_multipart_body_parts`
2025-11-06 21:09:42 +00:00
snipe
4d38bd1c62 Renamed variable 2025-11-06 21:07:52 +00:00
snipe
138262114d Added enctype back in 2025-11-06 21:04:55 +00:00
snipe
4073c9e638 Updated ordering 2025-11-06 21:01:20 +00:00
snipe
f1b4877a98 A few small tweaks for new groups 2025-11-06 20:36:51 +00:00
snipe
c39c92d0d7 Mash the ids into a string, fixed disclosure arrows 2025-11-06 20:12:25 +00:00
snipe
90fc48d959 Shim workaround to avoid max_input_vars and max_multipart_body_parts limits
max_input_vars = 2000
max_multipart_body_parts = 2048
2025-11-06 18:17:40 +00:00
Godfrey M
7f10a53105 dark blue 2025-11-05 12:47:10 -08:00
Godfrey M
7ae1b7a765 change border-color of create new 2025-11-05 12:40:29 -08:00
Godfrey M
22a43a0463 adds redirect option fix to accessories update method 2025-11-05 12:19:18 -08:00
Godfrey M
9348204987 remove spacing adjustment 2025-11-05 11:22:11 -08:00
Godfrey M
776c7caaa5 get warning/success status check to display 2025-11-05 11:19:05 -08:00
Godfrey M
2216b83ca7 get warning/success status check to display 2025-11-05 11:16:35 -08:00
snipe
dea399398a Merge pull request #18161 from Godmartinz/TZE_24mm_E_adjustment
FD-50838: Fixes 24mm_E label sizing
2025-11-05 18:50:42 +00:00
Godfrey M
7434dd9458 final adjustments to 24mm_E label 2025-11-05 09:32:21 -08:00
snipe
723abca34a Merge remote-tracking branch 'origin/develop' 2025-11-05 15:28:40 +00:00
snipe
88e532dbc4 Fixed #18157 - reports permission glitch 2025-11-05 15:24:15 +00:00
snipe
32b28327e9 Merge remote-tracking branch 'origin/develop' 2025-11-04 21:42:05 +00:00
snipe
c5ad451c39 Merge pull request #18156 from grokability/#18119-fix-double-helpering-on-dates-for-asset-acceptance
Fixed #18119 - double formatting for acceptance/decline date
2025-11-04 21:40:58 +00:00
snipe
44bfceeb0f Fixed #18119 - double formatting for acceptance/decline date 2025-11-04 21:36:27 +00:00
snipe
c30131275f Merge remote-tracking branch 'origin/develop' 2025-11-04 21:23:14 +00:00
snipe
37eb63837b Merge pull request #18155 from grokability/#18021-fix-patch-api-with-unique-serial
Override unique_undeleted in the form request
2025-11-04 21:23:00 +00:00
snipe
4ada47e3b0 Use new setting variable since we already have it 2025-11-04 21:12:49 +00:00
snipe
e5c55c9ab3 Fixed typo in the comment 2025-11-04 21:11:54 +00:00
snipe
547b3df7b4 Added more commentary on why we’re intefering with the request 2025-11-04 21:08:48 +00:00
snipe
4100f2600c Override unique_undeleted in the form request 2025-11-04 20:43:31 +00:00
snipe
56218dfcb2 Merge remote-tracking branch 'origin/develop' 2025-11-04 19:39:52 +00:00
snipe
a9574e8fd6 Fixed #18133 - make the disabled toggle JS so it’s clearer 2025-11-04 19:39:35 +00:00
snipe
0d2a75db0a Merge remote-tracking branch 'origin/develop' 2025-11-04 19:04:25 +00:00
snipe
c6269d6bbc Merge pull request #18151 from MarvelousAnything/fixes/test_webhook_content_type
Fix Content-Type Header not being set correctly for testWebhook
2025-11-04 19:03:53 +00:00
snipe
eb9d066844 Merge remote-tracking branch 'origin/develop' 2025-11-04 18:56:15 +00:00
snipe
c1204a5301 Merge pull request #18152 from grokability/#18020-rework-pr
Re-apply #18020, fixed #15107 (mostly) - added prefix and more options to 2D barcodes
2025-11-04 18:55:54 +00:00
snipe
cc5ac65909 Re-apply #18020, fixed #15107 (mostly) - added prefix and more options to label 2D tags 2025-11-04 18:43:35 +00:00
Owen Voskuhl Hayes
9ddc48e3d7 fix the headers field for Guzzle request 2025-11-04 12:03:21 -05:00
snipe
029f3030a7 Merge remote-tracking branch 'origin/develop' 2025-11-04 16:06:35 +00:00
snipe
4a39d7c67a Use transformer for dept update responses 2025-11-04 16:06:19 +00:00
snipe
b7a6706591 Merge pull request #18150 from grokability/#18148-dept-api-request-user-count
Fixed #18148 and #17451 - return int for user_count, fixed validation
2025-11-04 16:05:42 +00:00
snipe
ddb031f091 Fixed #18148 and #17451 - return int for user_count, fixed validation 2025-11-04 15:56:23 +00:00
snipe
e906d25776 Merge pull request #16973 from spencerrlongg/bug/sc-29245
Adds Form Request for Creating Departments
2025-11-04 15:09:38 +00:00
Brady Wetherington
27d7449459 Fix comment to properly reflext the current state of the database 2025-11-04 14:04:37 +00:00
snipe
8c59a8d405 Merged clipboard PR and generated prod assets 2025-11-03 17:39:41 +00:00
snipe
5d8905c997 Merge pull request #18143 from grokability/#18101-make-copy-to-clipboard-more-consistent
Fixed #18101: Make copy to clipboard alignment more consistent
2025-11-03 17:36:54 +00:00
snipe
517f4ce121 Fixed #18101: Make copy to clipboard alignment more consistent 2025-11-03 17:29:39 +00:00
snipe
6809bbd3d5 Merge pull request #18142 from grokability/#18136-copy-asset-name
Fixed #18136 - adds copy to clipbpard to asset name
2025-11-03 15:48:59 +00:00
snipe
ab555d05e1 Fixed #18136 - adds copy to clipbpard to asset name 2025-11-03 15:47:32 +00:00
Brady Wetherington
f7bc538fdf Intorduce ActionType enum and ensure all logactions are using it 2025-11-03 15:39:31 +00:00
Brady Wetherington
2de66ad5db Merge branch 'fix_incorrect_action_types' into actiontype_enum_redux 2025-11-03 15:31:14 +00:00
snipe
30e16b6213 Added int 2025-11-03 14:10:36 +00:00
snipe
92b50ca7ae Addresses #17994, #16925 2025-11-03 14:10:27 +00:00
snipe
9ee36df979 Merge pull request #18139 from grokability/#18082-add-company-to-seat-view
Fixed #18082: Added user company to checked out licenses
2025-11-03 12:51:29 +00:00
snipe
46d1c14e1a Added user company to checked out licenses 2025-11-03 12:45:33 +00:00
snipe
61895011fb Merge pull request #18131 from Godmartinz/fix-inventory-alert-notification-misfire
Adds a null check for items and threshold in inventory alert notification
2025-10-30 20:11:24 +00:00
Godfrey M
32d43034bd add a null check for items and threshold in inventory alert notification 2025-10-30 10:47:17 -07:00
snipe
dfc6cdc127 Merge pull request #18128 from marcusmoore/fixes/17738-category-edit-form-fix
Fixed #17738 - accurately represent checkbox on category edit screen
2025-10-30 16:43:17 +00:00
snipe
16e93f9e18 Merge remote-tracking branch 'origin/master' into develop 2025-10-30 14:03:23 +00:00
snipe
7395b1a4eb Track permission changes 2025-10-30 13:40:24 +00:00
snipe
fa98557225 Check that the permissions are really an array
This accounts for weird data in the permissions column
2025-10-30 13:34:50 +00:00
Marcus Moore
894606b62e Remove old tests 2025-10-29 13:16:03 -07:00
Marcus Moore
f0a6a0026a Improve phrasing 2025-10-29 13:03:37 -07:00
Marcus Moore
070e0c93be Improve phrasing 2025-10-29 12:46:26 -07:00
Marcus Moore
2b27b733e5 Improve wording 2025-10-29 12:45:45 -07:00
Marcus Moore
0355c2b642 Dynamically adjust checkbox wording 2025-10-29 12:44:34 -07:00
Marcus Moore
3bad19fb56 Improve translation key name 2025-10-29 12:40:39 -07:00
Marcus Moore
0f84d51a48 Improve property name 2025-10-29 12:37:17 -07:00
Marcus Moore
2e8572d9c5 Use v3 syntax for computed properties 2025-10-29 12:35:30 -07:00
Marcus Moore
df53d5d966 Skip computing sendCheckInEmail 2025-10-29 12:32:01 -07:00
Marcus Moore
23838959ca Never disable email checkbox 2025-10-29 12:27:12 -07:00
chruoss
6904ad02a2 Renamed L6009.php -> L4736.php
Renamed L6009_A.php -> L4736_A.php
Added correct L6009.php
Added correct L6009_A.php
2025-10-29 15:13:12 +01:00
Marcus Moore
777872d41f Add notification group 2025-10-23 13:53:38 -07:00
Marcus Moore
b85d1f184a Remove redundant test 2025-10-23 13:52:52 -07:00
Marcus Moore
02129eeddb Add try/catch 2025-10-23 13:51:53 -07:00
Marcus Moore
2612e0bbc8 Remove unused import 2025-10-23 12:43:12 -07:00
Marcus Moore
3670efacb4 Implement test 2025-10-23 12:39:36 -07:00
Marcus Moore
476611b70f Remove redundant test 2025-10-23 12:27:20 -07:00
Brady Wetherington
b96e2fb52c Added migration to fix incorrect action_type in action_log 2025-10-23 15:40:54 +01:00
Marcus Moore
60df2a17f8 Check context when sending to alert address 2025-10-22 16:41:36 -07:00
Marcus Moore
f64f4795c1 Send request instead of firing event 2025-10-22 16:39:34 -07:00
Marcus Moore
e036f756d5 Improve setup 2025-10-22 16:24:47 -07:00
Marcus Moore
92fd121cae Clean up 2025-10-22 16:12:35 -07:00
Marcus Moore
6307337892 Add scenario 2025-10-22 16:09:26 -07:00
Marcus Moore
fc2e35cd32 Improve assertions 2025-10-22 14:23:07 -07:00
Marcus Moore
1811e061aa Populate scenario 2025-10-22 14:21:24 -07:00
Marcus Moore
59037f0d83 Move scenario 2025-10-22 14:18:45 -07:00
Marcus Moore
67edb7d396 Send to alert email 2025-10-22 14:17:32 -07:00
Marcus Moore
0da393f950 Populate scenario 2025-10-22 14:02:16 -07:00
Marcus Moore
abd30e551e Clean up 2025-10-22 13:46:19 -07:00
Marcus Moore
6fb2889a92 Clean up 2025-10-22 13:45:00 -07:00
Marcus Moore
54125d27e0 Add scenario 2025-10-22 13:44:14 -07:00
Marcus Moore
41efda5f82 Add todos 2025-10-21 13:22:41 -07:00
Marcus Moore
2aee14a800 Only send mail to target if they have an email address 2025-10-21 13:14:50 -07:00
Marcus Moore
be69da0a0d Add test case 2025-10-21 13:13:45 -07:00
Marcus Moore
31a247b55b Add test case 2025-10-21 12:44:50 -07:00
Marcus Moore
33c156be16 Add failing test 2025-10-21 12:43:23 -07:00
Marcus Moore
fd66a083d6 Fix assertion 2025-10-21 12:41:51 -07:00
Marcus Moore
d276f50fdf Fix assertion 2025-10-21 12:30:27 -07:00
Marcus Moore
4cb748e124 Improve test assertions 2025-10-21 12:29:25 -07:00
Marcus Moore
503e6898c3 WIP 2025-10-20 16:53:36 -07:00
Marcus Moore
f34056fe2e Scaffold some testing changes 2025-10-20 16:35:27 -07:00
Marcus Moore
8ff3575442 Add test for listener registration 2025-10-20 16:31:52 -07:00
Marcus Moore
b5e3358bbd Add todos 2025-10-20 14:29:05 -07:00
Marcus Moore
047a1197be Add failing conditions 2025-10-20 14:18:11 -07:00
Marcus Moore
0e87843446 WIP: start testing 2025-10-16 14:30:48 -07:00
Marcus Moore
4f1ff328ad Display eula if it is the same for all items 2025-10-14 17:10:57 -07:00
Marcus Moore
ad5bbb9b37 Add divider 2025-10-14 16:34:18 -07:00
Marcus Moore
c6e2fd2cab Add note 2025-10-14 16:33:37 -07:00
Marcus Moore
d3a7e25b86 Move expected checking and only show once 2025-10-14 16:29:59 -07:00
Marcus Moore
062445a48e Add expected checkin 2025-10-14 16:25:32 -07:00
Marcus Moore
3c32be6181 Make introduction line dynamic 2025-10-14 16:22:19 -07:00
Marcus Moore
e2f4a9bf9f Make subject dynamic 2025-10-14 16:20:01 -07:00
Marcus Moore
2db4c1b2e4 Add todo 2025-10-14 16:19:42 -07:00
Marcus Moore
6ed93f4a4f Add asset details 2025-10-14 14:02:21 -07:00
Marcus Moore
9dcee71baf wip 2025-10-08 16:18:54 -07:00
Marcus Moore
9bdd0d1d1e Add admin name 2025-10-08 16:05:55 -07:00
Marcus Moore
13b51d8608 Make acceptance section dynamic 2025-10-08 15:46:36 -07:00
Marcus Moore
19969fee39 Update closing 2025-10-08 15:32:38 -07:00
Marcus Moore
28dc4bf52e Move template to correct directory 2025-10-08 14:13:04 -07:00
Marcus Moore
9a380ac3d4 Extract intro text 2025-10-08 14:12:35 -07:00
Marcus Moore
17a26b43f0 Naively send email 2025-10-08 14:01:07 -07:00
Marcus Moore
3c42acebf0 Scaffold new listener 2025-09-30 15:11:12 -07:00
Marcus Moore
3327b2ce3c Add assertions 2025-09-30 13:03:24 -07:00
Godfrey M
8c56aa9575 changed the wrong language file 2025-09-30 12:42:46 -07:00
Godfrey M
d528126f15 right hand table loading, need styling and translations 2025-09-30 12:31:44 -07:00
akemidx
64d0e3928c clean erroneous added space 2025-09-30 13:11:38 -04:00
Marcus Moore
7017a0cae1 WIP: introduce event 2025-09-25 16:23:43 -07:00
Marcus Moore
a40e4d7d04 Begin experimenting with context 2025-09-25 15:43:04 -07:00
Marcus Moore
9e3b56f4bc Add failing test 2025-09-25 15:41:37 -07:00
Godfrey Martinez
ca44ee94a4 Merge pull request #28 from Godmartinz/add_types_to_command
update the snipeit reminder command
2025-09-25 10:44:53 -07:00
Godfrey M
3ae7a77032 update the snipeit reminder command 2025-09-25 10:43:41 -07:00
Godfrey M
881c789a75 add usuage of withoutactionlog 2025-09-24 15:45:01 -07:00
Godfrey M
fea0189479 Merge branch 'fix-factory-auto-gen-action-logs' into add_types_to_unaccepted_asset_report 2025-09-24 15:43:27 -07:00
Godfrey M
6ca0e19819 uncomment code 2025-09-24 13:50:41 -07:00
Godfrey M
bf6964ee62 tests passing, CheckoutAcceptance Factory needs eyes though 2025-09-24 13:41:04 -07:00
Godfrey M
9ac2ea2a52 fix test to check all mailable types for reminders 2025-09-24 13:22:33 -07:00
Godfrey M
0ce20c1edd fix send reminder method to handle other types 2025-09-23 18:40:03 -07:00
Godfrey M
d31d99b40f remove else and blank lines 2025-09-23 17:00:08 -07:00
Godfrey M
3527c357cc whoops 2025-09-23 16:57:06 -07:00
Godfrey M
2b5254e68f fixed, eager loading, variable names, clean up 2025-09-23 16:55:10 -07:00
Godfrey M
6f3323c195 fix data in view model 2025-09-23 12:50:26 -07:00
Godfrey M
5af85bfe7d further clean up 2025-09-23 12:10:50 -07:00
Godfrey M
20adad3c6b fix company name link, clean up query 2025-09-23 12:09:21 -07:00
Godfrey M
ca8eae4064 updated the csv values to be plain 2025-09-23 11:40:29 -07:00
Godfrey M
7077faaf4a updated the postassetAcceptanceReport query and rows 2025-09-23 11:08:37 -07:00
Godfrey M
ab30df10ff remove sorter from blade 2025-09-22 12:20:32 -07:00
Godfrey M
a6cb75c481 remove sorter, didnt work 2025-09-22 12:19:34 -07:00
Godfrey M
51ce570eb3 attempt to sort chronologically, can not resort still 2025-09-18 11:01:40 -07:00
Godfrey M
dcbb09bbd7 added checkoutable class 2025-09-16 15:59:53 -07:00
Godfrey M
58eac619ea add checkoutable class, rework blade for unaccepted items 2025-09-16 15:52:41 -07:00
akemidx
3492ed54de i'm dumb, fixed query 2025-09-10 19:05:28 -04:00
akemidx
1455281957 updated to outside window 2025-09-10 18:49:04 -04:00
akemidx
e1de57384e field for number choice 2025-08-26 18:03:47 -04:00
akemidx
c95462328a selected value(s) 2025-08-19 18:35:07 -04:00
akemidx
84fc89250a backend works, no save yet 2025-08-19 14:30:26 -04:00
akemidx
a80f52cbf4 putting dropdown back 2025-08-18 19:47:26 -04:00
akemidx
7ce324a3a5 controller 2025-08-14 17:42:19 -04:00
akemidx
7624082b29 dropdown 2025-08-13 18:07:39 -04:00
akemidx
6d3bba696a first front end bit 2025-08-12 19:14:11 -04:00
spencerrlongg
1b397cd780 assertDbHas 2025-05-21 18:17:45 -05:00
spencerrlongg
120316bae0 wrong location import 2025-05-21 13:36:31 -05:00
spencerrlongg
7571ff007f add fillable properties to rules, some tests, move authorization to request 2025-05-21 12:51:21 -05:00
spencerrlongg
7afd7da2b4 initial work, more testing/tests needed 2025-05-21 11:44:54 -05:00
Matthias Mair
68dad1d3ae fixed wrong index reference in MoveUploadsToNewDisk.php
usage of undefined reference fixed
2023-08-22 20:39:21 +02:00
7085 changed files with 428976 additions and 222777 deletions

View File

@@ -4235,6 +4235,33 @@
"contributions": [
"code"
]
},
{
"login": "smarsching",
"name": "Sebastian Marsching",
"avatar_url": "https://avatars.githubusercontent.com/u/2880129?v=4",
"profile": "http://sebastian.marsching.com/",
"contributions": [
"code"
]
},
{
"login": "mohammad-ahmadi1",
"name": "Mo",
"avatar_url": "https://avatars.githubusercontent.com/u/40658372?v=4",
"profile": "https://github.com/mohammad-ahmadi1",
"contributions": [
"code"
]
},
{
"login": "MarvelousAnything",
"name": "Owen V. Hayes",
"avatar_url": "https://avatars.githubusercontent.com/u/20994684?v=4",
"profile": "https://github.com/MarvelousAnything",
"contributions": [
"code"
]
}
]
}

View File

@@ -137,6 +137,8 @@ PUBLIC_AWS_ACCESS_KEY_ID=null
PUBLIC_AWS_DEFAULT_REGION=null
PUBLIC_AWS_BUCKET=null
PUBLIC_AWS_URL=null
PUBLIC_AWS_ENDPOINT=null
PUBLIC_AWS_PATH_STYLE=null
PUBLIC_AWS_BUCKET_ROOT=null
# --------------------------------------------
@@ -147,6 +149,8 @@ PRIVATE_AWS_SECRET_ACCESS_KEY=null
PRIVATE_AWS_DEFAULT_REGION=null
PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null
PRIVATE_AWS_ENDPOINT=null
PRIVATE_AWS_PATH_STYLE=null
PRIVATE_AWS_BUCKET_ROOT=null
# --------------------------------------------

View File

@@ -144,6 +144,8 @@ PUBLIC_AWS_ACCESS_KEY_ID=null
PUBLIC_AWS_DEFAULT_REGION=null
PUBLIC_AWS_BUCKET=null
PUBLIC_AWS_URL=null
PUBLIC_AWS_ENDPOINT=null
PUBLIC_AWS_PATH_STYLE=null
PUBLIC_AWS_BUCKET_ROOT=null
# --------------------------------------------
@@ -154,6 +156,8 @@ PRIVATE_AWS_SECRET_ACCESS_KEY=null
PRIVATE_AWS_DEFAULT_REGION=null
PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null
PRIVATE_AWS_ENDPOINT=null
PRIVATE_AWS_PATH_STYLE=null
PRIVATE_AWS_BUCKET_ROOT=null
# --------------------------------------------

View File

@@ -40,12 +40,26 @@ DB_SANITIZE_BY_DEFAULT=false
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS
# --------------------------------------------
# Enable SSL connection to database (true/false)
DB_SSL=false
# Set to true for cloud databases like AWS RDS, Azure Database, Google Cloud SQL
# Set to false for self-hosted databases with client certificates
DB_SSL_IS_PAAS=false
# Required when DB_SSL_IS_PAAS=false (client certificate authentication)
DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
# Path to CA certificate bundle (required for SSL connections)
# For AWS RDS, download from: https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
DB_SSL_CA_PATH=null
# SSL cipher (optional, leave null for default)
DB_SSL_CIPHER=null
# Verify server certificate (true/false, defaults to false if not set)
# Set to false for development or when using self-signed certificates
DB_SSL_VERIFY_SERVER=null
# --------------------------------------------
@@ -143,6 +157,8 @@ PUBLIC_AWS_ACCESS_KEY_ID=null
PUBLIC_AWS_DEFAULT_REGION=null
PUBLIC_AWS_BUCKET=null
PUBLIC_AWS_URL=null
PUBLIC_AWS_ENDPOINT=null
PUBLIC_AWS_PATH_STYLE=null
PUBLIC_AWS_BUCKET_ROOT=null
# --------------------------------------------
@@ -153,6 +169,8 @@ PRIVATE_AWS_SECRET_ACCESS_KEY=null
PRIVATE_AWS_DEFAULT_REGION=null
PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null
PRIVATE_AWS_ENDPOINT=null
PRIVATE_AWS_PATH_STYLE=null
PRIVATE_AWS_BUCKET_ROOT=null
# --------------------------------------------
@@ -197,7 +215,7 @@ REPORT_TIME_LIMIT=12000
API_THROTTLE_PER_MINUTE=120
CSV_ESCAPE_FORMULAS=true
LIVEWIRE_URL_PREFIX=null
MAX_UNPAGINATED=5000
# --------------------------------------------
# OPTIONAL: SAML SETTINGS

View File

@@ -1,10 +1,11 @@
frontend: ["*.js", "*.css", "*.vue", "*.scss", "*.less", "*.blade.*", "resources/views/livewire/*"]
frontend: ["*.js", "*.css", "*.scss", "*.less", "*.blade.*", "resources/views/livewire/*","resources/views/layouts/default.blade.php"]
skins: ["*.js", "*.css", "*.scss", "*.less"]
css: ["*.css","*.scss", "*.less"]
javascript: ["*.js", "package.json", "package.lock"]
backend: ["/app/*", "composer.json", "composer.lock"]
translations: ["/resources/lang"]
translations: ["/resources/lang/*"]
livewire: ["/app/Http/Livewire/*", "resources/views/livewire/*"]
blade-components: ["resources/views/blade/*"]
backups: ["*backup*"]
restore: ["*restore*"]
saml: ["*saml*"]
@@ -16,7 +17,7 @@ api: ["/app/Http/Controllers/Api/*"]
notifications: ["/app/Notifications/*"]
importer: ["/app/Importer/*","/app/Http/Livewire/Importer.php", "resources/views/livewire/importer.php"]
cli / artisan: ["/app/Console/*"]
LDAP: ["*Ldap*", "/app/Console/Commands/Ldap*","/app/Models/Ldap.php"]
LDAP: ["*Ldap*", "/app/Console/Commands/Ldap*","/app/Models/Ldap.php", "/resources/views/users/ldap.blade.php","/resources/views/settings/ldap.blade.php"]
docker: ["*docker/*", "Dockerfile", "Dockerfile.alpine", "Dockerfile.fpm-alpine", ".dockerignore", ".env.docker"]
tests: ["/tests/*", "/database/factories/*", "/stubs"]
config: .github

117
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,117 @@
# GitHub Copilot Custom Instructions for Snipe-IT
These instructions guide Copilot to generate code that aligns with modern Laravel 11 standards, PHP 8.2/8.4 features, software engineering principles, and industry best practices to improve software quality, maintainability, and security.
## ✅ General Coding Standards
- Prefer short, expressive, and readable code.
- Use **meaningful, descriptive variable, function, class, and file names**.
- Apply proper PHPDoc blocks for classes, methods, and complex logic.
- Organize code into small, reusable functions or classes with single responsibility.
- Avoid magic numbers or hard-coded strings; use constants or config files.
## ✅ PHP 8.2/8.4 Best Practices
- Use **readonly properties** to enforce immutability where applicable.
- Use **Enums** instead of string or integer constants.
- Utilize **First-class callable syntax** for callbacks.
- Leverage **Constructor Property Promotion**.
- Use **Union Types**, **Intersection Types**, and **true/false return types** for strict typing.
- Apply **Static Return Type** where needed.
- Use the **Nullsafe Operator (?->)** for optional chaining.
- Adopt **final classes** where extension is not intended.
- Use **Named Arguments** for improved clarity when calling functions with multiple parameters.
## ✅ Laravel 11 Project Structure & Conventions
- Follow the official Laravel project structure:
- `app/Http/Controllers` - Controllers
- `app/Models` - Eloquent models
- `app/Http/Requests` - Form request validation
- `app/Http/Resources` - API resource responses
- `app/Enums` - Enums
- `app/Actions` - Single-responsibility action classes
- `app/Policies` - Authorization logic
- Controllers must:
- Use dependency injection.
- Use Form Requests for validation. The request class should utilize the rules set on the model.
- Return typed responses (e.g., `JsonResponse`).
- Use Transformers for API responses.
## ✅ Eloquent ORM & Database
- Use **Eloquent Models** with proper `$fillable` or `$guarded` attributes for mass assignment protection.
- Utilize **casts** for date, boolean, JSON, and custom data types.
- Apply **accessors & mutators** for attribute transformation.
- Avoid direct raw SQL unless absolutely necessary; prefer Eloquent or Query Builder.
- Migrations:
- Always use migrations for schema changes.
- Include proper constraints (foreign keys, unique indexes, etc.).
- Prefer UUIDs or ULIDs as primary keys where applicable.
## ✅ API Development
- Use **Transformer classes** for consistent and structured JSON responses.
- Apply **route model binding** where possible.
- Use Form Requests for input validation.
## ✅ Blade & Frontend (if applicable)
- Keep Blade templates clean and logic-free; use View Composers or dedicated View Models for complex data.
- Use `@props`, `@aware`, `@once` Blade features appropriately.
- Utilize Alpine.js or Livewire for interactive frontend logic (optional).
## ✅ Security Best Practices
- Never trust user input; always validate and sanitize inputs.
- Use prepared statements via Eloquent or Query Builder to prevent SQL injection.
- Use Laravel's built-in CSRF, XSS, and validation mechanisms.
- Store sensitive information in `.env`, never hard-code secrets.
- Apply proper authorization checks using Policies or Gates.
- Follow principle of least privilege for users, roles, and permissions.
## ✅ Testing Standards
- Use **factories** for test data setup.
- Include feature tests for user-facing functionality.
- Include unit tests for business logic, services, and helper classes.
- Mock external services using Laravel's `Http::fake()` or equivalent.
- Maintain high code coverage but focus on meaningful tests over 100% coverage obsession.
## ✅ Software Quality & Maintainability
- Follow **SOLID Principles**:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
- Follow **DRY** (Don't Repeat Yourself) and **KISS** (Keep It Simple, Stupid) principles.
- Apply **YAGNI** (You Aren't Gonna Need It) to avoid overengineering.
- Document complex logic with PHPDoc and inline comments.
## ✅ Performance & Optimization
- Eager load relationships to avoid N+1 queries.
- Use caching with Laravel's Cache system for frequently accessed data.
- Paginate large datasets using `paginate()` instead of `get()`.
- Queue long-running tasks using Laravel Queues.
- Optimize database indexes for common queries.
## ✅ Modern Laravel Features to Use
- Use **Event Broadcasting** if real-time updates are needed.
- Use **Full-text search** if search functionality is required.
- Use **Rate Limiting** for API routes.
## ✅ Additional Copilot Behavior Preferences
- Generate **strictly typed**, modern PHP code using latest language features.
- Prioritize **readable, clean, maintainable** code over cleverness.
- Avoid legacy or deprecated Laravel patterns (facade overuse, logic-heavy views, etc.).
- Suggest proper class placement based on Laravel directory structure.
- Suggest tests alongside new features where applicable.
- Default to **immutability**, **dependency injection**, and **encapsulation** best practices.
No newline at end of file

View File

@@ -26,7 +26,7 @@ jobs:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -1,57 +0,0 @@
# This workflow checks out code, performs a Codacy security scan
# and integrates the results with the
# GitHub Advanced Security code scanning feature. For more information on
# the Codacy security scan action usage and parameters, see
# https://github.com/codacy/codacy-analysis-cli-action.
# For more information on Codacy Analysis CLI in general, see
# https://github.com/codacy/codacy-analysis-cli.
name: Codacy Security Scan
on:
push:
branches: [ develop ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ develop ]
schedule:
- cron: '36 23 * * 3'
permissions:
contents: read
jobs:
codacy-security-scan:
# Ensure schedule job never runs on forked repos. It's only executed for 'grokability/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'))
name: Codacy Security Scan
runs-on: ubuntu-latest
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout code
uses: actions/checkout@v5
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@v4.4.7
with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
verbose: true
output: results.sarif
format: sarif
# Adjust severity of non-security issues
gh-code-scanning-compat: true
# Force 0 exit code to allow SARIF file generation
# This will handover control about PR rejection to the GitHub side
max-allowed-issues: 2147483647
# Upload the SARIF file generated in the previous step
- name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Crowdin push
uses: crowdin/github-action@v2

View File

@@ -42,17 +42,17 @@ jobs:
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v5
uses: actions/checkout@v6
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
@@ -64,7 +64,7 @@ jobs:
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }}
@@ -73,7 +73,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile.alpine

View File

@@ -42,17 +42,17 @@ jobs:
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v5
uses: actions/checkout@v6
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
@@ -64,7 +64,7 @@ jobs:
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }}
@@ -73,7 +73,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile

View File

@@ -11,7 +11,7 @@ jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Docker Hub Description
uses: grokability/dockerhub-description@7ea9d275c7cdbe2b676a093a0308c50665e3b8b4

View File

@@ -37,13 +37,13 @@ jobs:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -82,7 +82,7 @@ jobs:
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |

View File

@@ -34,13 +34,13 @@ jobs:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -81,7 +81,7 @@ jobs:
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |

View File

@@ -25,13 +25,13 @@ jobs:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -67,7 +67,7 @@ jobs:
- name: Upload Laravel logs as artifacts
if: always()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: laravel-logs-php-${{ matrix.php-version }}-run-${{ github.run_attempt }}
path: |

View File

@@ -68,7 +68,8 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/181059?v=4" width="110px;"/><br /><sub>Juan Font</sub>](https://github.com/juanfont)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juanfont "Code") | [<img src="https://avatars.githubusercontent.com/u/13137708?v=4" width="110px;"/><br /><sub>Juho Taipale</sub>](https://github.com/juhotaipale)<br />[💻](https://github.com/snipe/snipe-it/commits?author=juhotaipale "Code") | [<img src="https://avatars.githubusercontent.com/u/1007419?v=4" width="110px;"/><br /><sub>Korvin Szanto</sub>](https://github.com/KorvinSzanto)<br />[💻](https://github.com/snipe/snipe-it/commits?author=KorvinSzanto "Code") | [<img src="https://avatars.githubusercontent.com/u/8513053?v=4" width="110px;"/><br /><sub>Lewis Foster</sub>](https://lewisfoster.foo/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sniff122 "Code") | [<img src="https://avatars.githubusercontent.com/u/33877541?v=4" width="110px;"/><br /><sub>Logan Swartzendruber</sub>](https://github.com/loganswartz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=loganswartz "Code") | [<img src="https://avatars.githubusercontent.com/u/1156208?v=4" width="110px;"/><br /><sub>Lorenzo P.</sub>](https://github.com/lopezio)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lopezio "Code") | [<img src="https://avatars.githubusercontent.com/u/33946590?v=4" width="110px;"/><br /><sub>Lukas Jung</sub>](https://github.com/m4us1ne)<br />[💻](https://github.com/snipe/snipe-it/commits?author=m4us1ne "Code") |
| [<img src="https://avatars.githubusercontent.com/u/10965027?v=4" width="110px;"/><br /><sub>Ellie</sub>](https://leafedfox.xyz/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeafedFox "Code") | [<img src="https://avatars.githubusercontent.com/u/20960555?v=4" width="110px;"/><br /><sub>GA Stamper</sub>](https://github.com/gastamper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gastamper "Code") | [<img src="https://avatars.githubusercontent.com/u/206553556?v=4" width="110px;"/><br /><sub>Guillaume Lefranc</sub>](https://github.com/gl-pup)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gl-pup "Code") | [<img src="https://avatars.githubusercontent.com/u/733892?v=4" width="110px;"/><br /><sub>Hajo Möller</sub>](https://github.com/dasjoe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dasjoe "Code") | [<img src="https://avatars.githubusercontent.com/u/3420063?v=4" width="110px;"/><br /><sub>Istvan Basa</sub>](https://github.com/pottom)<br />[💻](https://github.com/snipe/snipe-it/commits?author=pottom "Code") | [<img src="https://avatars.githubusercontent.com/u/810824?v=4" width="110px;"/><br /><sub>JJ Asghar</sub>](https://jjasghar.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jjasghar "Code") | [<img src="https://avatars.githubusercontent.com/u/40404495?v=4" width="110px;"/><br /><sub>James E. Msenga</sub>](https://github.com/JemCdo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JemCdo "Code") |
| [<img src="https://avatars.githubusercontent.com/u/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [<img src="https://avatars.githubusercontent.com/u/634790?v=4" width="110px;"/><br /><sub>Nicky West</sub>](http://nickwest.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [<img src="https://avatars.githubusercontent.com/u/1347327?v=4" width="110px;"/><br /><sub>akaspeh1</sub>](https://github.com/akaspeh1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [<img src="https://avatars.githubusercontent.com/u/634790?v=4" width="110px;"/><br /><sub>Nicky West</sub>](http://nickwest.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [<img src="https://avatars.githubusercontent.com/u/1347327?v=4" width="110px;"/><br /><sub>akaspeh1</sub>](https://github.com/akaspeh1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") | [<img src="https://avatars.githubusercontent.com/u/2880129?v=4" width="110px;"/><br /><sub>Sebastian Marsching</sub>](http://sebastian.marsching.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=smarsching "Code") | [<img src="https://avatars.githubusercontent.com/u/40658372?v=4" width="110px;"/><br /><sub>Mo</sub>](https://github.com/mohammad-ahmadi1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mohammad-ahmadi1 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/20994684?v=4" width="110px;"/><br /><sub>Owen V. Hayes</sub>](https://github.com/MarvelousAnything)<br />[💻](https://github.com/snipe/snipe-it/commits?author=MarvelousAnything "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,35 +1,35 @@
FROM alpine:3.19
FROM alpine:3.23
# Apache + PHP
RUN apk add --no-cache \
apache2 \
php82 \
php82-common \
php82-apache2 \
php82-curl \
php82-ldap \
php82-mysqli \
php82-gd \
php82-xml \
php82-mbstring \
php82-zip \
php82-ctype \
php82-tokenizer \
php82-pdo_mysql \
php82-openssl \
php82-bcmath \
php82-phar \
php82-json \
php82-iconv \
php82-fileinfo \
php82-simplexml \
php82-session \
php82-dom \
php82-xmlwriter \
php82-xmlreader \
php82-sodium \
php82-redis \
php82-pecl-memcached \
php82-exif \
php84 \
php84-common \
php84-apache2 \
php84-curl \
php84-ldap \
php84-mysqli \
php84-gd \
php84-xml \
php84-mbstring \
php84-zip \
php84-ctype \
php84-tokenizer \
php84-pdo_mysql \
php84-openssl \
php84-bcmath \
php84-phar \
php84-json \
php84-iconv \
php84-fileinfo \
php84-simplexml \
php84-session \
php84-dom \
php84-xmlwriter \
php84-xmlreader \
php84-sodium \
php84-redis \
php84-pecl-memcached \
php84-exif \
curl \
wget \
vim \
@@ -42,7 +42,7 @@ COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
# Where apache's PID lives
RUN mkdir -p /run/apache2 && chown apache:apache /run/apache2
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php82/php.ini
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php84/php.ini
COPY docker/000-default-2.4.conf /etc/apache2/conf.d/default.conf
# Enable mod_rewrite

View File

@@ -1,6 +1,6 @@
![snipe-it-by-grok](https://github.com/grokability/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/804dd1beb14a41f38810ab77d64fc4fc)](https://app.codacy.com/gh/grokability/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests in MySQL](https://github.com/grokability/snipe-it/actions/workflows/tests-mysql.yml/badge.svg)](https://github.com/grokability/snipe-it/actions/workflows/tests-mysql.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
@@ -78,11 +78,14 @@ Since the release of the JSON REST API, several third-party developers have been
#### Libraries & Modules
- [SnipeScheduler](https://github.com/JSY-Ben/SnipeScheduler) by [@JSY-Ben](https://github.com/JSY-Ben) - An Asset Reservation/Checkout System for Snipe-IT
- [Snipe-IT MCP Server](https://github.com/jameshgordy/snipeit-mcp) by [@jameshgordy](https://github.com/jameshgordy) - A Model Context Protocol (MCP) server for managing Snipe-IT inventory systems
- [SnipeSharp - .NET module in C#](https://github.com/barrycarey/SnipeSharp) by [@barrycarey](https://github.com/barrycarey)
- [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
- [Snipe-IT plugin for Jira Service Desk](https://marketplace.atlassian.com/apps/1220964/snipe-it-for-jira)
- [Rudder2Snipe](https://github.com/norbertoaquino/rudder2snipe) by [@norbertoaquino](https://github.com/norbertoaquino) - Rudder.io integration for Snipe-IT
- [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.
@@ -121,7 +124,7 @@ We're currently working on our own mobile app, but in the meantime, check out th
### Contributing
**Please refrain from submitting issues or pull requests generated by fully-automated tools. Maintainers reserve the right, at their sole discretion, to close such submissions and to block any account responsible for them.**
**Please refrain from submitting issues or pull requests generated by fully-automated tools. Maintainers reserve the right, at their sole discretion, to close such submissions and to block any account responsible for them.** Please see our [AI Contribution Policy](https://snipe-it.readme.io/docs/contributing-overview#ai-usage-policy) for more information.
Contributions should follow from a human-to-human discussion in the form of an issue for the best chances of being merged into the core project. (Sometimes we might already be working on that feature, sometimes we've decided against )

View File

@@ -21,7 +21,7 @@ class DestroyCategoryAction
* @throws ItemStillHasLicenses
* @throws ItemStillHasConsumables
*/
static function run(Category $category): bool
public static function run(Category $category): bool
{
$category->loadCount([
'assets as assets_count',
@@ -29,7 +29,7 @@ class DestroyCategoryAction
'consumables as consumables_count',
'components as components_count',
'licenses as licenses_count',
'models as models_count'
'models as models_count',
]);
if ($category->assets_count > 0) {
@@ -56,4 +56,4 @@ class DestroyCategoryAction
return true;
}
}
}

View File

@@ -14,8 +14,8 @@ class CancelCheckoutRequestAction
{
public static function run(Asset $asset, User $user)
{
if (!Company::isCurrentUserHasAccess($asset)) {
throw new AuthorizationException();
if (! Company::isCurrentUserHasAccess($asset)) {
throw new AuthorizationException;
}
$asset->cancelRequest();
@@ -27,7 +27,7 @@ class CancelCheckoutRequestAction
$data['item_quantity'] = 1;
$settings = Setting::getSettings();
$logaction = new Actionlog();
$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');
@@ -44,5 +44,4 @@ class CancelCheckoutRequestAction
return true;
}
}
}

View File

@@ -23,8 +23,8 @@ class CreateCheckoutRequestAction
if (is_null(Asset::RequestableAssets()->find($asset->id))) {
throw new AssetNotRequestable($asset);
}
if (!Company::isCurrentUserHasAccess($asset)) {
throw new AuthorizationException();
if (! Company::isCurrentUserHasAccess($asset)) {
throw new AuthorizationException;
}
$data['item'] = $asset;
@@ -32,7 +32,7 @@ class CreateCheckoutRequestAction
$data['item_quantity'] = 1;
$settings = Setting::getSettings();
$logaction = new Actionlog();
$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');
@@ -51,4 +51,4 @@ class CreateCheckoutRequestAction
return true;
}
}
}

View File

@@ -20,7 +20,7 @@ class DeleteManufacturerAction
* @throws ItemStillHasLicenses
* @throws ItemStillHasConsumables
*/
static function run(Manufacturer $manufacturer): bool
public static function run(Manufacturer $manufacturer): bool
{
$manufacturer->loadCount([
'assets as assets_count',
@@ -55,9 +55,8 @@ class DeleteManufacturerAction
}
$manufacturer->delete();
//dd($manufacturer);
// dd($manufacturer);
return true;
}
}
}

View File

@@ -3,19 +3,18 @@
namespace App\Actions\Suppliers;
use App\Exceptions\ItemStillHasAccessories;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasComponents;
use App\Exceptions\ItemStillHasConsumables;
use App\Models\Supplier;
use App\Exceptions\ItemStillHasAssets;
use App\Exceptions\ItemStillHasMaintenances;
use App\Exceptions\ItemStillHasLicenses;
use App\Exceptions\ItemStillHasMaintenances;
use App\Models\Supplier;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class DestroySupplierAction
{
/**
*
* @throws ItemStillHasLicenses
* @throws ItemStillHasAssets
* @throws ItemStillHasMaintenances
@@ -23,7 +22,7 @@ class DestroySupplierAction
* @throws ItemStillHasConsumables
* @throws ItemStillHasComponents
*/
static function run(Supplier $supplier): bool
public static function run(Supplier $supplier): bool
{
$supplier->loadCount([
'maintenances as maintenances_count',

View File

@@ -4,9 +4,7 @@ namespace App\Console\Commands;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
class CheckinLicensesFromAllUsers extends Command
{

View File

@@ -3,10 +3,8 @@
namespace App\Console\Commands;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
class CheckoutLicenseToAllUsers extends Command
{
@@ -75,6 +73,7 @@ class CheckoutLicenseToAllUsers extends Command
if ($user->licenses->where('id', '=', $license_id)->count()) {
$this->info($user->username.' already has this license checked out to them. Skipping... ');
continue;
}

View File

@@ -21,7 +21,7 @@ class CleanIncorrectCheckoutAcceptances extends Command
*
* @var string
*/
protected $description = "Delete checkout acceptances for checkouts to non-users";
protected $description = 'Delete checkout acceptances for checkouts to non-users';
/**
* Execute the console command.
@@ -35,30 +35,32 @@ class CleanIncorrectCheckoutAcceptances extends Command
$this->withProgressBar(CheckoutAcceptance::all(), function ($checkoutAcceptance) use (&$deletions, &$skips) {
$item = $checkoutAcceptance->checkoutable;
$checkout_to_id = $checkoutAcceptance->assigned_to_id;
if(is_null($item)) {
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?
return; // 'false' allegedly breaks execution entirely, so 'true' maybe doesn't? hrm. just straight return maybe?
}
if(get_class($item) == LicenseSeat::class) {
if (get_class($item) == LicenseSeat::class) {
$item = $item->license;
}
foreach($item->assetlog()->where('action_type','checkout')->get() as $assetlog) {
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
// 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
// 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");
// $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);
// $this->info("No match! checkout to id: " . $checkout_to_id." target_id: ".$assetlog->target_id." target_type: ".$assetlog->target_type);
}
}
$skips++;

View File

@@ -8,6 +8,7 @@ use Illuminate\Console\Command;
class CleanOldCheckoutRequests extends Command
{
private int $deletions = 0;
private int $skips = 0;
/**
@@ -44,12 +45,14 @@ class CleanOldCheckoutRequests extends Command
if ($this->shouldForceDelete($request)) {
$request->forceDelete();
$this->deletions++;
return;
}
if ($this->shouldSoftDelete($request)) {
$request->delete();
$this->deletions++;
return;
}
@@ -64,7 +67,7 @@ class CleanOldCheckoutRequests extends Command
private function shouldForceDelete(CheckoutRequest $request)
{
// check if the requestable or user relationship is null
return !$request->requestable || !$request->user;
return ! $request->requestable || ! $request->user;
}
private function shouldSoftDelete(CheckoutRequest $request)

View File

@@ -2,31 +2,28 @@
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use \App\Models\User;
use Illuminate\Support\Carbon;
class CreateAdmin extends Command
{
/** @mixin User **/
/**
* App\Console\CreateAdmin
*
* @property mixed $first_name
* @property string $last_name
* @property string $username
* @property string $email
* @property string $permissions
* @property string $password
* @property boolean $activated
* @property boolean $show_in_list
* @property boolean $autoassign_licenses
* @property \Illuminate\Support\Carbon|null $created_at
* @property bool $activated
* @property bool $show_in_list
* @property bool $autoassign_licenses
* @property Carbon|null $created_at
* @property mixed $created_by
*/
protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?} {autoassign_licenses?}';
/**
@@ -46,7 +43,6 @@ class CreateAdmin extends Command
parent::__construct();
}
public function handle()
{
$first_name = $this->option('first_name');
@@ -57,8 +53,6 @@ class CreateAdmin extends Command
$show_in_list = $this->argument('show_in_list');
$autoassign_licenses = $this->argument('autoassign_licenses');
if (($first_name == '') || ($last_name == '') || ($username == '') || ($email == '') || ($password == '')) {
$this->info('ERROR: All fields are required.');
} else {

View File

@@ -24,6 +24,7 @@ class FixBulkAccessoryCheckinActionLogEntries extends Command
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;
/**
@@ -50,10 +51,11 @@ class FixBulkAccessoryCheckinActionLogEntries extends Command
if ($logs->isEmpty()) {
$this->info('No logs found with incorrect timestamps.');
return 0;
}
$this->info('Found ' . $logs->count() . ' logs with incorrect timestamps:');
$this->info('Found '.$logs->count().' logs with incorrect timestamps:');
$this->table(
['ID', 'Created By', 'Created At', 'Updated At'],
@@ -67,11 +69,11 @@ class FixBulkAccessoryCheckinActionLogEntries extends Command
})
);
if (!$this->dryrun && !$this->confirm('Update these logs?')) {
if (! $this->dryrun && ! $this->confirm('Update these logs?')) {
return 0;
}
if (!$this->dryrun && !$this->skipBackup) {
if (! $this->dryrun && ! $this->skipBackup) {
$this->info('Backing up the database before making changes...');
$this->call('snipeit:backup');
}
@@ -83,7 +85,7 @@ class FixBulkAccessoryCheckinActionLogEntries extends Command
foreach ($logs as $log) {
$this->newLine();
$this->info('Processing log id:' . $log->id);
$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
@@ -106,7 +108,7 @@ class FixBulkAccessoryCheckinActionLogEntries extends Command
$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) {
if (! $this->dryrun) {
Model::withoutTimestamps(function () use ($log) {
$log->saveQuietly();
});
@@ -129,7 +131,7 @@ class FixBulkAccessoryCheckinActionLogEntries extends Command
* 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
private function getCreatedByAttributeFromSimilarLog(Actionlog $log): ?int
{
$similarLog = Actionlog::query()
->whereNotNull('created_by')

View File

@@ -2,6 +2,21 @@
namespace App\Console\Commands;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Company;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\Department;
use App\Models\Depreciation;
use App\Models\Group;
use App\Models\License;
use App\Models\Location;
use App\Models\Manufacturer;
use App\Models\Statuslabel;
use App\Models\Supplier;
use App\Models\User;
use Illuminate\Console\Command;
class FixDoubleEscape extends Command
@@ -38,21 +53,21 @@ class FixDoubleEscape extends Command
public function handle()
{
$tables = [
\App\Models\Asset::class => ['name'],
\App\Models\License::class => ['name'],
\App\Models\Consumable::class => ['name'],
\App\Models\Accessory::class => ['name'],
\App\Models\Component::class => ['name'],
\App\Models\Company::class => ['name'],
\App\Models\Manufacturer::class => ['name'],
\App\Models\Supplier::class => ['name'],
\App\Models\Statuslabel::class => ['name'],
\App\Models\Depreciation::class => ['name'],
\App\Models\AssetModel::class => ['name'],
\App\Models\Group::class => ['name'],
\App\Models\Department::class => ['name'],
\App\Models\Location::class => ['name'],
\App\Models\User::class => ['first_name', 'last_name'],
Asset::class => ['name'],
License::class => ['name'],
Consumable::class => ['name'],
Accessory::class => ['name'],
Component::class => ['name'],
Company::class => ['name'],
Manufacturer::class => ['name'],
Supplier::class => ['name'],
Statuslabel::class => ['name'],
Depreciation::class => ['name'],
AssetModel::class => ['name'],
Group::class => ['name'],
Department::class => ['name'],
Location::class => ['name'],
User::class => ['first_name', 'last_name'],
];
$count = [];

View File

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\User;
use Illuminate\Console\Command;
class FixMismatchedAssetsAndLogs extends Command
@@ -56,26 +57,26 @@ class FixMismatchedAssetsAndLogs extends Command
$mismatch_count = 0;
$assets = Asset::whereNotNull('assigned_to')
->where('assigned_type', '=', \App\Models\User::class)
->where('assigned_type', '=', User::class)
->orderBy('id', 'ASC')->get();
foreach ($assets as $asset) {
// get the last checkout of the asset
if ($checkout_log = Actionlog::where('target_type', '=', \App\Models\User::class)
if ($checkout_log = Actionlog::where('target_type', '=', User::class)
->where('action_type', '=', 'checkout')
->where('item_id', '=', $asset->id)
->orderBy('created_at', 'DESC')
->first()) {
// Now check for a subsequent checkin log - we want to ignore those
if (! $checkin_log = Actionlog::where('target_type', '=', \App\Models\User::class)
->where('action_type', '=', 'checkin from')
->where('item_id', '=', $asset->id)
->whereDate('created_at', '>', $checkout_log->created_at)
->orderBy('created_at', 'DESC')
->first()) {
// Now check for a subsequent checkin log - we want to ignore those
if (! $checkin_log = Actionlog::where('target_type', '=', User::class)
->where('action_type', '=', 'checkin from')
->where('item_id', '=', $asset->id)
->whereDate('created_at', '>', $checkout_log->created_at)
->orderBy('created_at', 'DESC')
->first()) {
//print_r($asset);
// print_r($asset);
if ($checkout_log->target_id != $asset->assigned_to) {
$this->error('Log ID: '.$checkout_log->id.' -- Asset ID '.$checkout_log->item_id.' SHOULD BE checked out to User '.$checkout_log->target_id.' but its assigned_to is '.$asset->assigned_to);
@@ -90,7 +91,7 @@ class FixMismatchedAssetsAndLogs extends Command
$mismatch_count++;
}
} else {
//$this->info('Asset ID '.$asset->id.': There is a checkin '.$checkin_log->created_at.' after this checkout '.$checkout_log->created_at);
// $this->info('Asset ID '.$asset->id.': There is a checkin '.$checkin_log->created_at.' after this checkout '.$checkout_log->created_at);
}
}
}

View File

@@ -27,6 +27,6 @@ class FixUpAssignedTypeWithoutAssignedTo extends 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");
$this->info('Assets with an assigned_type but no assigned_to are fixed');
}
}

View File

@@ -27,40 +27,42 @@ class FixupAssignedToWithoutAssignedType extends Command
*/
public function handle()
{
$assets = Asset::whereNull("assigned_type")->whereNotNull("assigned_to")->withTrashed();
$assets = Asset::whereNull('assigned_type')->whereNotNull('assigned_to')->withTrashed();
$this->withProgressBar($assets->get(), function (Asset $asset) {
//now check each action log, from the most recent backwards, to find the last checkin or checkout
foreach($asset->log()->orderBy("id","desc")->get() as $action_log) {
if($this->option("debug")) {
$this->info("Asset id: " . $asset->id . " action log, action type is: " . $action_log->action_type);
// now check each action log, from the most recent backwards, to find the last checkin or checkout
foreach ($asset->log()->orderBy('id', 'desc')->get() as $action_log) {
if ($this->option('debug')) {
$this->info('Asset id: '.$asset->id.' action log, action type is: '.$action_log->action_type);
}
switch($action_log->action_type) {
switch ($action_log->action_type) {
case 'checkin from':
if($this->option("debug")) {
$this->info("Doing a checkin for ".$asset->id);
if ($this->option('debug')) {
$this->info('Doing a checkin for '.$asset->id);
}
$asset->assigned_to = null;
// if you have a required custom field, we still want to save, and we *don't* want an action_log
$asset->saveQuietly();
return;
case 'checkout':
if($this->option("debug")) {
$this->info("Doing a checkout for " . $asset->id . " picking target type: " . $action_log->target_type);
if ($this->option('debug')) {
$this->info('Doing a checkout for '.$asset->id.' picking target type: '.$action_log->target_type);
}
if($asset->assigned_to != $action_log->target_id) {
$this->error("Asset's assigned_to does *NOT* match Action Log's target_id. \$asset->assigned_to=".$asset->assigned_to." vs. \$action_log->target_id=".$action_log->target_id);
//FIXME - do we abort here? Do we try to keep looking? I don't know, this means your data is *really* messed up...
if ($asset->assigned_to != $action_log->target_id) {
$this->error("Asset's assigned_to does *NOT* match Action Log's target_id. \$asset->assigned_to=".$asset->assigned_to.' vs. $action_log->target_id='.$action_log->target_id);
// FIXME - do we abort here? Do we try to keep looking? I don't know, this means your data is *really* messed up...
}
$asset->assigned_type = $action_log->target_type;
$asset->saveQuietly(); // see above
return;
}
}
$asset->assigned_to = null; //asset was never checked in or out in its lifetime - it stays 'checked in'
$asset->saveQuietly(); //see above
$asset->assigned_to = null; // asset was never checked in or out in its lifetime - it stays 'checked in'
$asset->saveQuietly(); // see above
});
$this->newLine();
$this->info("Assets assigned_type are fixed");
$this->info('Assets assigned_type are fixed');
}
}

View File

@@ -2,14 +2,13 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use Laravel\Passport\TokenRepository;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Laravel\Passport\TokenRepository;
class GeneratePersonalAccessToken extends Command
{
/**
* The name and signature of the console command.
*
@@ -27,15 +26,13 @@ class GeneratePersonalAccessToken extends Command
*/
protected $description = 'This console command allows you to generate Personal API tokens to be used with the Snipe-IT JSON REST API on behalf of a user.';
/**
* The token repository implementation.
*
* @var \Laravel\Passport\TokenRepository
* @var TokenRepository
*/
protected $tokenRepository;
/**
* Create a new command instance.
*
@@ -56,11 +53,11 @@ class GeneratePersonalAccessToken extends Command
{
$accessTokenName = $this->option('name');
if ($accessTokenName=='') {
if ($accessTokenName == '') {
$accessTokenName = 'CLI Auth Token';
}
if ($this->option('user_id')=='') {
if ($this->option('user_id') == '') {
return $this->error('ERROR: user_id cannot be blank.');
}
@@ -75,7 +72,7 @@ class GeneratePersonalAccessToken extends Command
$this->warn('Your API Token has been created. Be sure to copy this token now, as it WILL NOT be accessible again.');
if ($token = DB::table('oauth_access_tokens')->where('user_id', '=', $user->id)->where('name','=',$accessTokenName)->orderBy('created_at', 'desc')->first()) {
if ($token = DB::table('oauth_access_tokens')->where('user_id', '=', $user->id)->where('name', '=', $accessTokenName)->orderBy('created_at', 'desc')->first()) {
$this->info('API Token ID: '.$token->id);
}
@@ -84,11 +81,8 @@ class GeneratePersonalAccessToken extends Command
$this->info('API Token: '.$createAccessToken);
}
} else {
return $this->error('ERROR: Invalid user. API key was not created.');
return $this->error('ERROR: Invalid user. API key was not created.');
}
}
}

View File

@@ -46,7 +46,7 @@ class ImportLocations extends Command
$filename = $this->argument('filename');
$csv = Reader::createFromPath(storage_path('private_uploads/imports/').$filename, 'r');
$this->info('Attempting to process: '.storage_path('private_uploads/imports/').$filename);
$csv->setHeaderOffset(0); //because we don't want to insert the header
$csv->setHeaderOffset(0); // because we don't want to insert the header
$results = $csv->getRecords();
// Import parent location names first if they don't exist

View File

@@ -38,22 +38,23 @@ class KillAllSessions extends Command
public function handle()
{
if (!$this->option('force') && !$this->confirm("****************************************************\nTHIS WILL FORCE A LOGIN FOR ALL LOGGED IN USERS.\n\nAre you SURE you wish to continue? ")) {
return $this->error("Session loss not confirmed");
if (! $this->option('force') && ! $this->confirm("****************************************************\nTHIS WILL FORCE A LOGIN FOR ALL LOGGED IN USERS.\n\nAre you SURE you wish to continue? ")) {
return $this->error('Session loss not confirmed');
}
$session_files = glob(storage_path("framework/sessions/*"));
$session_files = glob(storage_path('framework/sessions/*'));
$count = 0;
foreach ($session_files as $file) {
if (is_file($file))
if (is_file($file)) {
unlink($file);
$count++;
}
$count++;
}
\DB::table('users')->update(['remember_token' => null]);
$this->info($count. ' sessions cleared!');
$this->info($count.' sessions cleared!');
}
}

View File

@@ -5,11 +5,11 @@ namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\Department;
use App\Models\Group;
use Illuminate\Console\Command;
use App\Models\Setting;
use App\Models\Ldap;
use App\Models\User;
use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class LdapSync extends Command
@@ -47,35 +47,34 @@ class LdapSync extends Command
{
// If LDAP enabled isn't set to 1 (ldap_enabled!=1) then we should cut this short immediately without going any further
if (Setting::getSettings()->ldap_enabled!='1') {
if (Setting::getSettings()->ldap_enabled != '1') {
$this->error('LDAP is not enabled. Aborting. See Settings > LDAP to enable it.');
exit();
}
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); // 600 seconds = 10 minutes
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
// Map the LDAP attributes to the Snipe-IT user fields.
$ldap_map = [
"username" => Setting::getSettings()->ldap_username_field,
"last_name" => Setting::getSettings()->ldap_lname_field,
"first_name" => Setting::getSettings()->ldap_fname_field,
"active_flag" => Setting::getSettings()->ldap_active_flag,
"emp_num" => Setting::getSettings()->ldap_emp_num,
"email" => Setting::getSettings()->ldap_email,
"phone" => Setting::getSettings()->ldap_phone_field,
"mobile" => Setting::getSettings()->ldap_mobile,
"jobtitle" => Setting::getSettings()->ldap_jobtitle,
"address" => Setting::getSettings()->ldap_address,
"city" => Setting::getSettings()->ldap_city,
"state" => Setting::getSettings()->ldap_state,
"zip" => Setting::getSettings()->ldap_zip,
"country" => Setting::getSettings()->ldap_country,
"location" => Setting::getSettings()->ldap_location,
"dept" => Setting::getSettings()->ldap_dept,
"manager" => Setting::getSettings()->ldap_manager,
"display_name" => Setting::getSettings()->ldap_display_name,
'username' => Setting::getSettings()->ldap_username_field,
'last_name' => Setting::getSettings()->ldap_lname_field,
'first_name' => Setting::getSettings()->ldap_fname_field,
'active_flag' => Setting::getSettings()->ldap_active_flag,
'emp_num' => Setting::getSettings()->ldap_emp_num,
'email' => Setting::getSettings()->ldap_email,
'phone' => Setting::getSettings()->ldap_phone_field,
'mobile' => Setting::getSettings()->ldap_mobile,
'jobtitle' => Setting::getSettings()->ldap_jobtitle,
'address' => Setting::getSettings()->ldap_address,
'city' => Setting::getSettings()->ldap_city,
'state' => Setting::getSettings()->ldap_state,
'zip' => Setting::getSettings()->ldap_zip,
'country' => Setting::getSettings()->ldap_country,
'location' => Setting::getSettings()->ldap_location,
'dept' => Setting::getSettings()->ldap_dept,
'manager' => Setting::getSettings()->ldap_manager,
'display_name' => Setting::getSettings()->ldap_display_name,
];
$ldap_default_group = Setting::getSettings()->ldap_default_group;
@@ -101,13 +100,13 @@ class LdapSync extends Command
/**
* if a location ID has been specified, use that OU
*/
if ( $this->option('location_id') ) {
if ($this->option('location_id')) {
foreach($this->option('location_id') as $location_id){
foreach ($this->option('location_id') as $location_id) {
$location_ou = Location::where('id', '=', $location_id)->value('ldap_ou');
$search_base = $location_ou;
Log::debug('Importing users from specified location OU: \"'.$search_base.'\".');
}
}
}
/**
@@ -153,21 +152,21 @@ class LdapSync extends Command
$default_location = null;
if ($this->option('location') != '') {
if ($default_location = Location::where('name', '=', $this->option('location'))->first()) {
Log::debug('Location name ' . $this->option('location') . ' passed');
Log::debug('Location name '.$this->option('location').' passed');
Log::debug('Importing to '.$default_location->name.' ('.$default_location->id.')');
}
} elseif ($this->option('location_id')) {
//TODO - figure out how or why this is an array?
foreach($this->option('location_id') as $location_id) {
// TODO - figure out how or why this is an array?
foreach ($this->option('location_id') as $location_id) {
if ($default_location = Location::where('id', '=', $location_id)->first()) {
Log::debug('Location ID ' . $location_id . ' passed');
Log::debug('Location ID '.$location_id.' passed');
Log::debug('Importing to '.$default_location->name.' ('.$default_location->id.')');
}
}
}
if (!isset($default_location)) {
if (! isset($default_location)) {
Log::debug('That location is invalid or a location was not provided, so no location will be assigned by default.');
}
@@ -208,17 +207,17 @@ class LdapSync extends Command
}
$usernames = [];
for ($i = 0; $i < $location_users['count']; $i++) {
if (array_key_exists($ldap_map["username"], $location_users[$i])) {
if (array_key_exists($ldap_map['username'], $location_users[$i])) {
$location_users[$i]['ldap_location_override'] = true;
$location_users[$i]['location_id'] = $ldap_loc['id'];
$usernames[] = $location_users[$i][$ldap_map["username"]][0];
$usernames[] = $location_users[$i][$ldap_map['username']][0];
}
}
// Delete located users from the general group.
foreach ($results as $key => $generic_entry) {
if ((is_array($generic_entry)) && (array_key_exists($ldap_map["username"], $generic_entry))) {
if (in_array($generic_entry[$ldap_map["username"]][0], $usernames)) {
if ((is_array($generic_entry)) && (array_key_exists($ldap_map['username'], $generic_entry))) {
if (in_array($generic_entry[$ldap_map['username']][0], $usernames)) {
unset($results[$key]);
}
}
@@ -232,42 +231,41 @@ class LdapSync extends Command
$manager_cache = [];
if($ldap_default_group != null) {
if ($ldap_default_group != null) {
$default = Group::find($ldap_default_group);
if (!$default) {
if (! $default) {
$ldap_default_group = null; // un-set the default group if that group doesn't exist
}
}
// Assign the mapped LDAP attributes for each user to the Snipe-IT user fields
for ($i = 0; $i < $results['count']; $i++) {
$item = [];
$item['username'] = $results[$i][$ldap_map["username"]][0] ?? '';
$item['display_name'] = $results[$i][$ldap_map["display_name"]][0] ?? '';
$item['employee_number'] = $results[$i][$ldap_map["emp_num"]][0] ?? '';
$item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? '';
$item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? '';
$item['email'] = $results[$i][$ldap_map["email"]][0] ?? '';
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
$item['location_id'] = $results[$i]['location_id'] ?? '';
$item['telephone'] = $results[$i][$ldap_map["phone"]][0] ?? '';
$item['mobile'] = $results[$i][$ldap_map["mobile"]][0] ?? '';
$item['jobtitle'] = $results[$i][$ldap_map["jobtitle"]][0] ?? '';
$item['address'] = $results[$i][$ldap_map["address"]][0] ?? '';
$item['city'] = $results[$i][$ldap_map["city"]][0] ?? '';
$item['state'] = $results[$i][$ldap_map["state"]][0] ?? '';
$item['country'] = $results[$i][$ldap_map["country"]][0] ?? '';
$item['zip'] = $results[$i][$ldap_map["zip"]][0] ?? '';
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
$item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? '';
$item['location'] = $results[$i][$ldap_map["location"]][0] ?? '';
$location = $default_location; //initially, set '$location' to the default_location (which may just be `null`)
$item['username'] = $results[$i][$ldap_map['username']][0] ?? null;
$item['display_name'] = $results[$i][$ldap_map['display_name']][0] ?? null;
$item['employee_number'] = $results[$i][$ldap_map['emp_num']][0] ?? null;
$item['lastname'] = $results[$i][$ldap_map['last_name']][0] ?? null;
$item['firstname'] = $results[$i][$ldap_map['first_name']][0] ?? null;
$item['email'] = $results[$i][$ldap_map['email']][0] ?? null;
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? null;
$item['location_id'] = $results[$i]['location_id'] ?? null;
$item['telephone'] = $results[$i][$ldap_map['phone']][0] ?? null;
$item['mobile'] = $results[$i][$ldap_map['mobile']][0] ?? null;
$item['jobtitle'] = $results[$i][$ldap_map['jobtitle']][0] ?? null;
$item['address'] = $results[$i][$ldap_map['address']][0] ?? null;
$item['city'] = $results[$i][$ldap_map['city']][0] ?? null;
$item['state'] = $results[$i][$ldap_map['state']][0] ?? null;
$item['country'] = $results[$i][$ldap_map['country']][0] ?? null;
$item['zip'] = $results[$i][$ldap_map['zip']][0] ?? null;
$item['department'] = $results[$i][$ldap_map['dept']][0] ?? null;
$item['manager'] = $results[$i][$ldap_map['manager']][0] ?? null;
$item['location'] = $results[$i][$ldap_map['location']][0] ?? null;
$location = $default_location; // initially, set '$location' to the default_location (which may just be null)
// ONLY if you are using the "ldap_location" option *AND* you have an actual result
if ($ldap_map["location"] && $item['location']) {
if ($ldap_map['location'] && $item['location']) {
$location = Location::firstOrCreate([
'name' => $item['location'],
]);
@@ -289,58 +287,58 @@ class LdapSync extends Command
$item['createorupdate'] = 'created';
}
//If a sync option is not filled in on the LDAP settings don't populate the user field
if($ldap_map["username"] != null){
// If a sync option is not filled in on the LDAP settings don't populate the user field
if ($ldap_map['username'] != null) {
$user->username = $item['username'];
}
if($ldap_map["display_name"] != null){
if ($ldap_map['display_name'] != null) {
$user->display_name = $item['display_name'];
}
if($ldap_map["last_name"] != null){
if ($ldap_map['last_name'] != null) {
$user->last_name = $item['lastname'];
}
if($ldap_map["first_name"] != null){
if ($ldap_map['first_name'] != null) {
$user->first_name = $item['firstname'];
}
if($ldap_map["emp_num"] != null){
if ($ldap_map['emp_num'] != null) {
$user->employee_num = e($item['employee_number']);
}
if($ldap_map["email"] != null){
if ($ldap_map['email'] != null) {
$user->email = $item['email'];
}
if($ldap_map["phone"] != null){
if ($ldap_map['phone'] != null) {
$user->phone = $item['telephone'];
}
if($ldap_map["mobile"] != null){
if ($ldap_map['mobile'] != null) {
$user->mobile = $item['mobile'];
}
if($ldap_map["jobtitle"] != null){
if ($ldap_map['jobtitle'] != null) {
$user->jobtitle = $item['jobtitle'];
}
if($ldap_map["address"] != null){
if ($ldap_map['address'] != null) {
$user->address = $item['address'];
}
if($ldap_map["city"] != null){
if ($ldap_map['city'] != null) {
$user->city = $item['city'];
}
if($ldap_map["state"] != null){
if ($ldap_map['state'] != null) {
$user->state = $item['state'];
}
if($ldap_map["country"] != null){
if ($ldap_map['country'] != null) {
$user->country = $item['country'];
}
if($ldap_map["zip"] != null){
if ($ldap_map['zip'] != null) {
$user->zip = $item['zip'];
}
if($ldap_map["dept"] != null){
if ($ldap_map['dept'] != null) {
$user->department_id = $department->id;
}
if($ldap_map["location"] != null){
if ($ldap_map['location'] != null) {
$user->location_id = $location?->id;
}
if($ldap_map["manager"] != null){
if($item['manager'] != null) {
if ($ldap_map['manager'] != null) {
if ($item['manager'] != null) {
// Check Cache first
if (isset($manager_cache[$item['manager']])) {
// found in cache; use that and avoid extra lookups
@@ -350,23 +348,23 @@ class LdapSync extends Command
try {
$ldap_manager = Ldap::findLdapUsers($item['manager'], -1, $this->option('filter'));
} catch (\Exception $e) {
Log::warning("Manager lookup caused an exception: " . $e->getMessage() . ". Falling back to direct username lookup");
Log::warning('Manager lookup caused an exception: '.$e->getMessage().'. Falling back to direct username lookup');
// Hail-mary for Okta manager 'shortnames' - will only work if
// Okta configuration is using full email-address-style usernames
$ldap_manager = [
"count" => 1,
'count' => 1,
0 => [
$ldap_map["username"] => [$item['manager']]
]
$ldap_map['username'] => [$item['manager']],
],
];
}
$add_manager_to_cache = true;
if ($ldap_manager["count"] > 0) {
if ($ldap_manager['count'] > 0) {
try {
// Get the Manager's username
// PHP LDAP returns every LDAP attribute as an array, and 90% of the time it's an array of just one item. But, hey, it's an array.
$ldapManagerUsername = $ldap_manager[0][$ldap_map["username"]][0];
$ldapManagerUsername = $ldap_manager[0][$ldap_map['username']][0];
// Get User from Manager username.
$ldap_manager = User::where('username', $ldapManagerUsername)->first();
@@ -377,11 +375,11 @@ class LdapSync extends Command
}
} catch (\Exception $e) {
$add_manager_to_cache = false;
\Log::warning('Handling ldap manager ' . $item['manager'] . ' caused an exception: ' . $e->getMessage() . '. Continuing synchronization.');
\Log::warning('Handling ldap manager '.$item['manager'].' caused an exception: '.$e->getMessage().'. Continuing synchronization.');
}
}
if ($add_manager_to_cache) {
$manager_cache[$item['manager']] = $ldap_manager && isset($ldap_manager->id) ? $ldap_manager->id : null; // Store results in cache, even if 'failed'
$manager_cache[$item['manager']] = $ldap_manager && isset($ldap_manager->id) ? $ldap_manager->id : null; // Store results in cache, even if 'failed'
}
}
@@ -389,18 +387,18 @@ class LdapSync extends Command
}
// Sync activated state for Active Directory.
if (!empty($ldap_map["active_flag"])) { // IF we have an 'active' flag set....
if (! empty($ldap_map['active_flag'])) { // IF we have an 'active' flag set....
// ....then *most* things that are truthy will activate the user. Anything falsey will deactivate them.
// (Specifically, we don't handle a value of '0.0' correctly)
$raw_value = @$results[$i][$ldap_map["active_flag"]][0];
$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);
} else {
$user->activated = $filter_var ?? $boolean_cast; // if filter_var() was true or false, use that. If it's null, use the $boolean_cast
}
@@ -408,7 +406,6 @@ class LdapSync extends Command
// ....otherwise, (ie if no 'active' LDAP flag is defined), IF the UAC setting exists,
// ....then use the UAC setting on the account to determine can-log-in vs. cannot-log-in
/* The following is _probably_ the correct logic, but we can't use it because
some users may have been dependent upon the previous behavior, and this
could cause additional access to be available to users they don't want
@@ -434,7 +431,7 @@ class LdapSync extends Command
'262688', // 0x40220 NORMAL_ACCOUNT, PASSWD_NOTREQD, SMARTCARD_REQUIRED
'328192', // 0x50200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
'328224', // 0x50220 NORMAL_ACCOUNT, PASSWD_NOT_REQD, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
'4194816',// 0x400200 NORMAL_ACCOUNT, DONT_REQ_PREAUTH
'4194816', // 0x400200 NORMAL_ACCOUNT, DONT_REQ_PREAUTH
'4260352', // 0x410200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, DONT_REQ_PREAUTH
'1049088', // 0x100200 NORMAL_ACCOUNT, NOT_DELEGATED
'1114624', // 0x110200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, NOT_DELEGATED,
@@ -445,14 +442,13 @@ class LdapSync extends Command
} /* implied 'else' here - leave the $user->activated flag alone. Newly-created accounts will be active.
already-existing accounts will be however the administrator has set them */
if ($item['ldap_location_override'] == true) {
$user->location_id = $item['location_id'];
} elseif ((isset($location)) && (!empty($location))) {
} elseif ((isset($location)) && (! empty($location))) {
if ((is_array($location)) && (array_key_exists('id', $location))) {
$user->location_id = $location['id'];
} elseif (is_object($location)) {
$user->location_id = $location->id; //THIS is the magic line, this should do it.
$user->location_id = $location->id; // THIS is the magic line, this should do it.
}
}
// TODO - should we be NULLING locations if $location is really `null`, and that's what we came up with?
@@ -464,16 +460,17 @@ class LdapSync extends Command
$errors = '';
if ($user->save()) {
$item['id'] = $user->id;
$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);
// Check if the relationship already exists
if (! $user->groups()->where('group_id', $ldap_default_group)->exists()) {
$user->groups()->attach($ldap_default_group);
}
}
//updates assets location based on user's location
// updates assets location based on user's location
if ($user->wasChanged('location_id')) {
foreach ($user->assets as $asset) {
$asset->location_id = $user->location_id;

View File

@@ -2,29 +2,32 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Ldap;
use App\Models\Setting;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Crypt;
use App\Models\Ldap;
/**
* Check if a given ip is in a network
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
*
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return bool true if the ip is in this range / false if not.
*/
function ip_in_range( $ip, $range ) {
if ( strpos( $range, '/' ) == false ) {
$range .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $range, $netmask ) = explode( '/', $range, 2 );
$range_decimal = ip2long( $range );
$ip_decimal = ip2long( $ip );
$wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
function ip_in_range($ip, $range)
{
if (strpos($range, '/') == false) {
$range .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
[$range, $netmask] = explode('/', $range, 2);
$range_decimal = ip2long($range);
$ip_decimal = ip2long($ip);
$wildcard_decimal = pow(2, (32 - $netmask)) - 1;
$netmask_decimal = ~$wildcard_decimal;
return ($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal);
}
// NOTE - this function was shamelessly stolen from this gist: https://gist.github.com/tott/7684443
@@ -33,10 +36,10 @@ function ip_in_range( $ip, $range ) {
*/
function parenthesized_filter($filter)
{
if(substr($filter,0,1) == "(" ) {
if (substr($filter, 0, 1) == '(') {
return $filter;
} else {
return "(".$filter.")";
return '('.$filter.')';
}
}
@@ -74,41 +77,44 @@ class LdapTroubleshooter extends Command
/**
* Output something *only* if debug is enabled
*
*
* @return void
*/
public function debugout($string)
{
if($this->option('debug')) {
if ($this->option('debug')) {
$this->line($string);
}
}
/**
* Clean the results from ldap_get_entries into something useful
* @param array $array
*
* @param array $array
* @return array
*/
public function ldap_results_cleaner ($array) {
public function ldap_results_cleaner($array)
{
$cleaned = [];
for($i = 0; $i < $array['count']; $i++) {
for ($i = 0; $i < $array['count']; $i++) {
$row = $array[$i];
$clean_row = [];
foreach($row AS $key => $val ) {
$this->debugout("Key is: ".$key);
if($key == "count" || is_int($key) || $key == "dn") {
foreach ($row as $key => $val) {
$this->debugout('Key is: '.$key);
if ($key == 'count' || is_int($key) || $key == 'dn') {
$this->debugout(" and we're gonna skip it\n");
continue;
}
$this->debugout(" And that seems fine.\n");
if(array_key_exists('count',$val)) {
if($val['count'] == 1) {
if (array_key_exists('count', $val)) {
if ($val['count'] == 1) {
$clean_row[$key] = $val[0];
} else {
unset($val['count']); //these counts are annoying
unset($val['count']); // these counts are annoying
$elements = [];
foreach($val as $entry) {
if(isset($ldap_constants[$entry])) {
foreach ($val as $entry) {
if (isset($ldap_constants[$entry])) {
$elements[] = $ldap_constants[$entry];
} else {
$elements[] = $entry;
@@ -122,6 +128,7 @@ class LdapTroubleshooter extends Command
}
$cleaned[$i] = $clean_row;
}
return $cleaned;
}
@@ -132,58 +139,58 @@ class LdapTroubleshooter extends Command
*/
public function handle()
{
if($this->option('trace')) {
ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 7);
if ($this->option('trace')) {
ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7);
}
$settings = Setting::getSettings();
$this->settings = $settings;
if($this->option('ldap-search')) {
if(!$this->option('force')) {
if ($this->option('ldap-search')) {
if (! $this->option('force')) {
$confirmation = $this->confirm('WARNING: This command will display your LDAP password on your terminal. Are you sure this is ok?');
if(!$confirmation) {
if (! $confirmation) {
$this->error('ABORTING');
exit(-1);
}
}
$output = [];
if($settings->ldap_server_cert_ignore) {
$this->line("# Ignoring server certificate validity");
$output[] = "LDAPTLS_REQCERT=never";
if ($settings->ldap_server_cert_ignore) {
$this->line('# Ignoring server certificate validity');
$output[] = 'LDAPTLS_REQCERT=never';
}
if($settings->ldap_client_tls_cert && $settings->ldap_client_tls_key) {
$this->line("# Adding LDAP Client Certificate and Key");
$output[] = "LDAPTLS_CERT=storage/ldap_client_tls.cert";
$output[] = "LDAPTLS_KEY=storage/ldap_client_tls.key";
if ($settings->ldap_client_tls_cert && $settings->ldap_client_tls_key) {
$this->line('# Adding LDAP Client Certificate and Key');
$output[] = 'LDAPTLS_CERT=storage/ldap_client_tls.cert';
$output[] = 'LDAPTLS_KEY=storage/ldap_client_tls.key';
}
$output[] = "ldapsearch";
$output[] = "-H ".$settings->ldap_server;
$output[] = "-x";
$output[] = "-b ".escapeshellarg($settings->ldap_basedn);
$output[] = "-D ".escapeshellarg($settings->ldap_uname);
$output[] = 'ldapsearch';
$output[] = '-H '.$settings->ldap_server;
$output[] = '-x';
$output[] = '-b '.escapeshellarg($settings->ldap_basedn);
$output[] = '-D '.escapeshellarg($settings->ldap_uname);
try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
} catch (Exception $e) {
$this->warn('Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.');
exit(0);
}
$output[] = "-w ". escapeshellarg($w);
$output[] = '-w '.escapeshellarg($w);
$output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter));
if($settings->ldap_tls) {
$this->line("# adding STARTTLS option");
$output[] = "-Z";
if ($settings->ldap_tls) {
$this->line('# adding STARTTLS option');
$output[] = '-Z';
}
$output[] = "-v";
$output[] = '-v';
$this->line("\n");
$this->line(implode(" \\\n",$output));
$this->line(implode(" \\\n", $output));
exit(0);
}
//PHP Version check for warning
// PHP Version check for warning
$php_version = phpversion();
list($major, $minor, $patch) = explode('.', $php_version);
[$major, $minor, $patch] = explode('.', $php_version);
if (
$major < 8 ||
($major == 8 && $minor < 3) ||
@@ -191,22 +198,22 @@ class LdapTroubleshooter extends Command
($major == 8 && $minor == 4 && $patch < 7)
) {
$this->warn("PHP Version: $php_version WARNING - Versions before 8.3.21 or 8.4.7 will return INCONSISTENT results!");
if (!$this->confirm("Are you sure you wish to continue?")) {
$this->warn("ABORTING");
if (! $this->confirm('Are you sure you wish to continue?')) {
$this->warn('ABORTING');
exit(-1);
}
}
if(!$this->option('force')) {
if (! $this->option('force')) {
$confirmation = $this->confirm('WARNING: This command will make several attempts to connect to your LDAP server. Are you sure this is ok?');
if(!$confirmation) {
if (! $confirmation) {
$this->error('ABORTING');
exit(-1);
}
}
//$this->line(print_r($settings,true));
$this->line("STAGE 1: Checking settings");
if(!$settings->ldap_enabled) {
// $this->line(print_r($settings,true));
$this->line('STAGE 1: Checking settings');
if (! $settings->ldap_enabled) {
$this->error("WARNING: Snipe-IT's LDAP setting is not turned on. (That may be OK if you're still trying to figure out settings)");
}
@@ -214,123 +221,126 @@ class LdapTroubleshooter extends Command
try {
$ldap_conn = ldap_connect($settings->ldap_server);
} catch (Exception $e) {
$this->error("WARNING: Exception caught when executing 'ldap_connect()' - ".$e->getMessage().". We will try to guess.");
$this->error("WARNING: Exception caught when executing 'ldap_connect()' - ".$e->getMessage().'. We will try to guess.');
}
if(!$ldap_conn) {
$this->error("WARNING: LDAP Server setting of: ".$settings->ldap_server." cannot be parsed. We will try to guess.");
//exit(-1);
if (! $ldap_conn) {
$this->error('WARNING: LDAP Server setting of: '.$settings->ldap_server.' cannot be parsed. We will try to guess.');
// exit(-1);
}
//since we never use $ldap_conn again, we don't have to ldap_unbind() it (it's not even connected, tbh - that only happens at bind-time)
// since we never use $ldap_conn again, we don't have to ldap_unbind() it (it's not even connected, tbh - that only happens at bind-time)
$parsed = parse_url($settings->ldap_server);
if(@$parsed['scheme'] != 'ldap' && @$parsed['scheme'] != 'ldaps') {
if (@$parsed['scheme'] != 'ldap' && @$parsed['scheme'] != 'ldaps') {
$this->error("WARNING: LDAP URL Scheme of '".@$parsed['scheme']."' is probably incorrect; should usually be ldap or ldaps");
}
if(!@$parsed['host']) {
$this->error("ERROR: Cannot determine hostname or IP from ldap URL: ".$settings->ldap_server.". ABORTING.");
if (! @$parsed['host']) {
$this->error('ERROR: Cannot determine hostname or IP from ldap URL: '.$settings->ldap_server.'. ABORTING.');
exit(-1);
} else {
$this->info("Determined LDAP hostname to be: ".$parsed['host']);
$this->info('Determined LDAP hostname to be: '.$parsed['host']);
}
$raw_ips = [];
if (inet_pton($parsed['host']) !== false) {
$this->line($parsed['host'] . " already looks like an address; skipping DNS lookup");
$this->line($parsed['host'].' already looks like an address; skipping DNS lookup');
$raw_ips[] = $parsed['host'];
} else {
$this->line("Performing DNS lookup of: " . $parsed['host']);
$this->line('Performing DNS lookup of: '.$parsed['host']);
$ips = dns_get_record($parsed['host']);
//$this->info("Host IP is: ".print_r($ips,true));
// $this->info("Host IP is: ".print_r($ips,true));
if (!$ips || count($ips) == 0) {
$this->error("ERROR: DNS lookup of host: " . $parsed['host'] . " has failed. ABORTING.");
if (! $ips || count($ips) == 0) {
$this->error('ERROR: DNS lookup of host: '.$parsed['host'].' has failed. ABORTING.');
exit(-1);
}
$this->debugout("IP's? " . print_r($ips, true));
$this->debugout("IP's? ".print_r($ips, true));
foreach ($ips as $ip) {
if (!isset($ip['ip'])) {
if (! isset($ip['ip'])) {
continue;
}
$raw_ips[] = $ip['ip'];
}
}
foreach ($raw_ips as $ip) {
if ($ip == "127.0.0.1") {
$this->error("WARNING: Using the localhost IP as the LDAP server. This is usually wrong");
if ($ip == '127.0.0.1') {
$this->error('WARNING: Using the localhost IP as the LDAP server. This is usually wrong');
}
if (ip_in_range($ip, '10.0.0.0/8') || ip_in_range($ip, '192.168.0.0/16') || ip_in_range($ip, '172.16.0.0/12')) {
$this->error("WARNING: Using an RFC1918 Private address for LDAP server. This may be correct, but it can be a problem if your Snipe-IT instance is not hosted on your private network");
$this->error('WARNING: Using an RFC1918 Private address for LDAP server. This may be correct, but it can be a problem if your Snipe-IT instance is not hosted on your private network');
}
}
$this->line("STAGE 2: Checking basic network connectivity");
$this->line('STAGE 2: Checking basic network connectivity');
$ports = [636, 389];
if(@$parsed['port'] && !in_array($parsed['port'],$ports)) {
if (@$parsed['port'] && ! in_array($parsed['port'], $ports)) {
$ports[] = $parsed['port'];
}
$open_ports=[];
foreach($ports as $port ) {
$open_ports = [];
foreach ($ports as $port) {
$errno = 0;
$errstr = '';
$timeout = 30.0;
$result = '';
$this->line("Attempting to connect to port: " . $port . " - may take up to $timeout seconds");
$this->line('Attempting to connect to port: '.$port." - may take up to $timeout seconds");
try {
$result = fsockopen($parsed['host'], $port, $errno, $errstr, 30.0);
} catch(Exception $e) {
$this->error("Exception: ".$e->getMessage());
} catch (Exception $e) {
$this->error('Exception: '.$e->getMessage());
}
if($result) {
$this->info("Success!");
if ($result) {
$this->info('Success!');
$open_ports[] = $port;
} else {
$this->error("WARNING: Cannot connect to port: $port - $errstr ($errno)");
}
}
if(count($open_ports) == 0) {
$this->error("ERROR - no open ports. ABORTING.");
if (count($open_ports) == 0) {
$this->error('ERROR - no open ports. ABORTING.');
exit(-1);
}
$this->line("STAGE 3: Determine encryption algorithm, if any");
$this->line('STAGE 3: Determine encryption algorithm, if any');
$ldap_urls = []; // [url, cert-check?, start_tls?]
$pretty_ldap_urls = [];
foreach($open_ports as $port) {
foreach ($open_ports as $port) {
$this->line("Trying TLS first for port $port");
$ldap_url = "ldaps://".$parsed['host'].":$port";
if($this->test_anonymous_bind($ldap_url)) {
$ldap_url = 'ldaps://'.$parsed['host'].":$port";
if ($this->test_anonymous_bind($ldap_url)) {
$this->info("Anonymous bind succesful to $ldap_url!");
$ldap_urls[] = [ $ldap_url, true, false ];
$pretty_ldap_urls[] = [$ldap_url, "enabled", "n/a (no)"];
$ldap_urls[] = [$ldap_url, true, false];
$pretty_ldap_urls[] = [$ldap_url, 'enabled', 'n/a (no)'];
continue; // TODO - lots of copypasta in these if(test_anonymous_bind()) routines...
} else {
$this->error("WARNING: Failed to bind to $ldap_url - trying without certificate checks.");
}
if($this->test_anonymous_bind($ldap_url, false)) {
if ($this->test_anonymous_bind($ldap_url, false)) {
$this->info("Anonymous bind successful to $ldap_url with certificate-checks disabled");
$ldap_urls[] = [$ldap_url, false, false];
$pretty_ldap_urls[] = [$ldap_url, "DISABLED", "n/a (no)"];
$pretty_ldap_urls[] = [$ldap_url, 'DISABLED', 'n/a (no)'];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with certificate checks disabled. Trying unencrypted with STARTTLS");
}
// now switching to ldap:// URL's from ldaps://
$ldap_url = "ldap://".$parsed['host'].":$port";
$ldap_url = 'ldap://'.$parsed['host'].":$port";
if($this->test_anonymous_bind($ldap_url, true, true)) {
if ($this->test_anonymous_bind($ldap_url, true, true)) {
$this->info("Plain connection to $ldap_url with STARTTLS succesful!");
$ldap_urls[] = [ $ldap_url, true, true ];
$pretty_ldap_urls[] = [$ldap_url, "enabled", "STARTTLS ENABLED"];
$ldap_urls[] = [$ldap_url, true, true];
$pretty_ldap_urls[] = [$ldap_url, 'enabled', 'STARTTLS ENABLED'];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without certificate checks.");
@@ -339,224 +349,235 @@ class LdapTroubleshooter extends Command
if ($this->test_anonymous_bind($ldap_url, false, true)) {
$this->info("Plain connection to $ldap_url with STARTTLS and cert checks *disabled* successful!");
$ldap_urls[] = [$ldap_url, false, true];
$pretty_ldap_urls[] = [$ldap_url, "DISABLED", "STARTTLS ENABLED"];
$pretty_ldap_urls[] = [$ldap_url, 'DISABLED', 'STARTTLS ENABLED'];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled, and cert checks disabled. Trying without STARTTLS");
}
if($this->test_anonymous_bind($ldap_url)) {
if ($this->test_anonymous_bind($ldap_url)) {
$this->info("Plain connection to $ldap_url succesful!");
$ldap_urls[] = [ $ldap_url, true, false ];
$pretty_ldap_urls[] = [$ldap_url, "n/a", "starttls disabled"];
$ldap_urls[] = [$ldap_url, true, false];
$pretty_ldap_urls[] = [$ldap_url, 'n/a', 'starttls disabled'];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url. Giving up on port $port");
}
}
$this->debugout(print_r($ldap_urls,true));
$this->debugout(print_r($ldap_urls, true));
if(count($ldap_urls) > 0 ) {
if (count($ldap_urls) > 0) {
$this->debugout("Found working LDAP URL's: ");
foreach($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead?
$this->debugout("LDAP URL: " . $ldap_url[0]);
$this->debugout($ldap_url[0] . ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled") . ($ldap_url[2] ? " STARTTLS Enabled " : " STARTTLS Disabled"));
foreach ($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead?
$this->debugout('LDAP URL: '.$ldap_url[0]);
$this->debugout($ldap_url[0].($ldap_url[1] ? ' certificate checks enabled' : ' certificate checks disabled').($ldap_url[2] ? ' STARTTLS Enabled ' : ' STARTTLS Disabled'));
}
$this->table(["URL", "Cert Checks?", "STARTTLS?"], $pretty_ldap_urls);
$this->table(['URL', 'Cert Checks?', 'STARTTLS?'], $pretty_ldap_urls);
} else {
$this->error("ERROR - no valid LDAP URL's available - ABORTING");
exit(1);
}
$this->line("STAGE 4: Test Administrative Bind for LDAP Sync");
foreach($ldap_urls AS $ldap_url) {
$this->line('STAGE 4: Test Administrative Bind for LDAP Sync');
foreach ($ldap_urls as $ldap_url) {
try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
} catch (Exception $e) {
$this->warn('Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.');
exit(0);
}
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, $w);
}
$this->line("STAGE 5: Test BaseDN");
//grab all LDAP_ constants and fill up a reversed array mapping from weird LDAP dotted-strings to (Constant Name)
$this->line('STAGE 5: Test BaseDN');
// grab all LDAP_ constants and fill up a reversed array mapping from weird LDAP dotted-strings to (Constant Name)
$all_defined_constants = get_defined_constants();
$ldap_constants = [];
foreach($all_defined_constants AS $key => $val) {
if(starts_with($key,"LDAP_") && is_string($val)) {
foreach ($all_defined_constants as $key => $val) {
if (starts_with($key, 'LDAP_') && is_string($val)) {
$ldap_constants[$val] = $key; // INVERT the meaning here!
}
}
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
$this->debugout('LDAP constants are: '.print_r($ldap_constants, true));
foreach($ldap_urls AS $ldap_url) {
foreach ($ldap_urls as $ldap_url) {
try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
} catch (Exception $e) {
$this->warn('Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.');
exit(0);
}
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,$w,$settings)) {
$this->info("Success getting informational bind!");
if ($this->test_informational_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, $w, $settings)) {
$this->info('Success getting informational bind!');
} else {
$this->error("Unable to get information from bind.");
$this->error('Unable to get information from bind.');
}
}
$this->line("STAGE 6: Test LDAP Login to Snipe-IT");
foreach($ldap_urls AS $ldap_url) {
$this->line("Starting auth to " . $ldap_url[0]);
while(true) {
$with_tls = $ldap_url[1] ? "with": "without";
$with_startssl = $ldap_url[2] ? "using": "not using";
if(!$this->confirm('Do you wish to try to authenticate to this directory: '.$ldap_url[0]." $with_tls TLS and $with_startssl STARTSSL?")) {
$this->line('STAGE 6: Test LDAP Login to Snipe-IT');
foreach ($ldap_urls as $ldap_url) {
$this->line('Starting auth to '.$ldap_url[0]);
while (true) {
$with_tls = $ldap_url[1] ? 'with' : 'without';
$with_startssl = $ldap_url[2] ? 'using' : 'not using';
if (! $this->confirm('Do you wish to try to authenticate to this directory: '.$ldap_url[0]." $with_tls TLS and $with_startssl STARTSSL?")) {
break;
}
$username = $this->ask("Username");
$password = $this->secret("Password");
$username = $this->ask('Username');
$password = $this->secret('Password');
$results = $this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results?
if ($results) {
$this->info("Success authenticating with " . $username);
$this->info('Success authenticating with '.$username);
} else {
$this->error("Unable to authenticate with " . $username);
$this->error('Unable to authenticate with '.$username);
}
}
}
$this->info("LDAP TROUBLESHOOTING COMPLETE!");
$this->info('LDAP TROUBLESHOOTING COMPLETE!');
}
public function connect_to_ldap($ldap_url, $check_cert, $start_tls)
public function connect_to_ldap($ldap_url, $check_cert, $start_tls)
{
if ($check_cert) {
$this->line("we *ARE* checking certs");
$this->line('we *ARE* checking certs');
Ldap::ignoreCertificates(false);
} else {
$this->line("we are IGNORING certs");
$this->line('we are IGNORING certs');
Ldap::ignoreCertificates(true);
}
$lconn = ldap_connect($ldap_url);
ldap_set_option($lconn, LDAP_OPT_PROTOCOL_VERSION, 3); // should we 'test' different protocol versions here? Does anyone even use anything other than LDAPv3?
// no - it's formally deprecated: https://tools.ietf.org/html/rfc3494
if($this->settings->ldap_client_tls_cert && $this->settings->ldap_client_tls_key) {
// no - it's formally deprecated: https://tools.ietf.org/html/rfc3494
if ($this->settings->ldap_client_tls_cert && $this->settings->ldap_client_tls_key) {
// client-side TLS certificate support for LDAP (Google Secure LDAP)
putenv('LDAPTLS_CERT=storage/ldap_client_tls.cert');
putenv('LDAPTLS_KEY=storage/ldap_client_tls.key');
}
if($start_tls) {
if(!ldap_start_tls($lconn)) {
$this->error("WARNING: Unable to start TLS");
if ($start_tls) {
if (! ldap_start_tls($lconn)) {
$this->error('WARNING: Unable to start TLS');
return false;
}
}
if(!$lconn) {
$this->error("WARNING: Failed to generate connection string - using: ".$ldap_url);
if (! $lconn) {
$this->error('WARNING: Failed to generate connection string - using: '.$ldap_url);
return false;
}
$net = ldap_set_option($lconn, LDAP_OPT_NETWORK_TIMEOUT, $this->option('timeout'));
$time = ldap_set_option($lconn, LDAP_OPT_TIMELIMIT, $this->option('timeout'));
if(!$net || !$time) {
$this->error("Unable to set timeouts!");
if (! $net || ! $time) {
$this->error('Unable to set timeouts!');
}
return $lconn;
}
public function test_anonymous_bind($ldap_url, $check_cert = true, $start_tls = false)
{
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert , $start_tls) {
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls) {
try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$this->line("Attempting to bind now, this can take a while if we mess it up");
$this->line('Attempting to bind now, this can take a while if we mess it up');
$bind_results = ldap_bind($lconn);
$this->line("Bind results are: " . $bind_results . " which translate into boolean: " . (bool)$bind_results);
$this->line('Bind results are: '.$bind_results.' which translate into boolean: '.(bool) $bind_results);
ldap_close($lconn);
return (bool)$bind_results;
return (bool) $bind_results;
} catch (Exception $e) {
$this->error("WARNING: Exception caught during bind - ".$e->getMessage());
$this->error('WARNING: Exception caught during bind - '.$e->getMessage());
return false;
}
});
}
public function test_authed_bind($ldap_url, $check_cert, $start_tls, $username, $password)
public function test_authed_bind($ldap_url, $check_cert, $start_tls, $username, $password)
{
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls, $username, $password) {
try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$bind_results = ldap_bind($lconn, $username, $password);
ldap_close($lconn);
if(!$bind_results) {
if (! $bind_results) {
$this->error("WARNING: Failed to bind to $ldap_url as $username");
return false;
} else {
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
return (bool)$lconn;
return (bool) $lconn;
}
} catch (Exception $e) {
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
return false;
}
});
}
public function test_informational_bind($ldap_url, $check_cert, $start_tls, $username, $password,$settings)
public function test_informational_bind($ldap_url, $check_cert, $start_tls, $username, $password, $settings)
{
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls, $username, $password, $settings) {
try { // TODO - copypasta'ed from test_authed_bind
$conn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$bind_results = ldap_bind($conn, $username, $password);
if(!$bind_results) {
if (! $bind_results) {
$this->error("WARNING: Failed to bind to $ldap_url as $username");
return false;
}
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
$cleaned_results = [];
try {
// This _may_ only work for Active Directory?
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl'] */);
$results = ldap_get_entries($conn, $result);
$cleaned_results = $this->ldap_results_cleaner($results);
//$this->line(print_r($cleaned_results,true));
// $this->line(print_r($cleaned_results,true));
$default_naming_contexts = $cleaned_results[0]['namingcontexts'];
$this->info("Default Naming Contexts:");
$this->info(implode(", ", $default_naming_contexts));
//okay, great - now how do we display those results? I have no idea.
} catch (\Exception $e) {
$this->info('Default Naming Contexts:');
$this->info(implode(', ', $default_naming_contexts));
// okay, great - now how do we display those results? I have no idea.
} catch (Exception $e) {
$this->error("Unable to get base naming contexts - here's what we *did* get:");
$this->line(print_r($cleaned_results, true));
}
// I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it?
$this->debugout("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
$this->debugout("Base DN is: ".$settings->ldap_basedn." and filter is: ".parenthesized_filter($settings->ldap_filter));
$this->debugout('Base DN is: '.$settings->ldap_basedn.' and filter is: '.parenthesized_filter($settings->ldap_filter));
$search_results = ldap_search($conn, $settings->ldap_basedn, parenthesized_filter($settings->ldap_filter));
$entries = ldap_get_entries($conn, $search_results);
$this->info("Printing first 10 results: ");
$this->info('Printing first 10 results: ');
$pretty_data = array_slice($this->ldap_results_cleaner($entries), 0, 10);
//print_r($data);
// print_r($data);
$headers = [];
foreach ($pretty_data as $row) {
//populate headers
// populate headers
foreach ($row as $key => $value) {
//skip objectsid and objectguid because it junks up output
if ($key == "objectsid" || $key == "objectguid") {
// skip objectsid and objectguid because it junks up output
if ($key == 'objectsid' || $key == 'objectguid') {
continue;
}
if (!in_array($key, $headers)) {
if (! in_array($key, $headers)) {
$headers[] = $key;
}
}
}
$table = [];
//repeat again to populate table
// repeat again to populate table
foreach ($pretty_data as $row) {
$newrow = [];
foreach ($headers as $header) {
if (is_array(@$row[$header])) {
$newrow[] = "[" . implode(", ", $row[$header]) . "]";
$newrow[] = '['.implode(', ', $row[$header]).']';
} else {
$newrow[] = @$row[$header];
}
@@ -565,8 +586,9 @@ class LdapTroubleshooter extends Command
}
$this->table($headers, $table);
} catch (\Exception $e) {
} catch (Exception $e) {
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
return false;
} finally {
ldap_close($conn);
@@ -575,53 +597,54 @@ class LdapTroubleshooter extends Command
}
/***********************************************
*
* This function executes $function - which is expected to be some kind of executable function -
*
* This function executes $function - which is expected to be some kind of executable function -
* with a timeout set. It respects the timeout by forking execution and setting a strict timer
* for which to get back a SIGUSR1 or SIGUSR2 signal from the forked process.
*
*
***********************************************/
private function timed_boolean_execute($function)
{
if(!(function_exists('pcntl_sigtimedwait') && function_exists('posix_getpid') && function_exists('pcntl_fork') && function_exists('posix_kill') && function_exists('pcntl_wifsignaled'))) {
if (! (function_exists('pcntl_sigtimedwait') && function_exists('posix_getpid') && function_exists('pcntl_fork') && function_exists('posix_kill') && function_exists('pcntl_wifsignaled'))) {
// POSIX functions needed for forking aren't present, just run the function inline (ignoring timeout)
$this->line('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
return $function();
} else {
$parent_pid = posix_getpid();
$pid = pcntl_fork();
switch($pid) {
switch ($pid) {
case 0:
//we're the 'child'
if($function()) {
//SUCCESS = SIGUSR1
// we're the 'child'
if ($function()) {
// SUCCESS = SIGUSR1
posix_kill($parent_pid, SIGUSR1);
} else {
//FAILURE = SIGUSR2
// FAILURE = SIGUSR2
posix_kill($parent_pid, SIGUSR2);
}
exit();
break; //yes I know we don't need it.
break; // yes I know we don't need it.
case -1:
//couldn't fork
$this->error("COULD NOT FORK - assuming failure");
// couldn't fork
$this->error('COULD NOT FORK - assuming failure');
return false;
break; //I still know that we don't need it
break; // I still know that we don't need it
default:
//we remain the 'parent', $pid is the PID of the forked process.
// we remain the 'parent', $pid is the PID of the forked process.
$siginfo = [];
$exit_status = pcntl_sigtimedwait ([SIGUSR1, SIGUSR2], $siginfo, $this->option('timeout'));
$exit_status = pcntl_sigtimedwait([SIGUSR1, SIGUSR2], $siginfo, $this->option('timeout'));
if ($exit_status == SIGUSR1) {
return true;
} else {
posix_kill($pid, SIGKILL); //make sure we don't have processes hanging around that might try and send signals during later executions, confusing us
posix_kill($pid, SIGKILL); // make sure we don't have processes hanging around that might try and send signals during later executions, confusing us
return false;
}
break; //Yeah I get it already, shush.
break; // Yeah I get it already, shush.
}
}
}
}

View File

@@ -44,19 +44,15 @@ class MergeUsersByUsername extends Command
$users = User::where('username', 'LIKE', '%@%')->whereNull('deleted_at')->get();
$this->info($users->count().' total non-deleted users whose usernames contain a @ symbol.');
foreach ($users as $user) {
$parts = explode('@', trim($user->username));
$this->info('Checking against username '.trim($parts[0]).'.');
$bad_users = User::where('username', '=', trim($parts[0]))
->whereNull('deleted_at')
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations','uploads', 'acceptances')
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations', 'uploads', 'acceptances')
->get();
foreach ($bad_users as $bad_user) {
$this->info($bad_user->username.' ('.$bad_user->id.') will be merged into '.$user->username.' ('.$user->id.') ');
@@ -125,7 +121,6 @@ class MergeUsersByUsername extends Command
event(new UserMerged($bad_user, $user, null));
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Console\Commands;
use App\Enums\ActionType;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class MigrateLicenseSeatQuantitiesInActionLogs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:migrate-license-seat-quantities-in-action-logs
{--no-interaction: Do not ask any interactive question}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Updates quantity field in action_logs table for license seats that were added or deleted.';
/**
* Execute the console command.
*/
public function handle()
{
$query = DB::table('action_logs')
->whereIn('action_type', [
ActionType::AddSeats->value,
ActionType::DeleteSeats->value,
])
->where('quantity', '=', 1)
->orderBy('id');
$count = $query->count();
if ($count === 0) {
$this->info('Nothing to update');
return 0;
}
$this->info("{$count} logs to update");
if ($this->option('no-interaction') || $this->confirm('Update quantities in the action log?')) {
$query->chunk(50, function ($logs) {
$logs->each(function ($log) {
$quantityFromNote = Str::between($log->note, 'ed ', ' seats');
if (! is_numeric($quantityFromNote)) {
$this->error('Could not parse quantity from ID: {id}', ['id' => $log->id]);
}
if ($log->quantity !== (int) $quantityFromNote) {
$this->info(vsprintf('Updating id: %s to quantity %s', [
'id' => $log->id,
'new_quantity' => $quantityFromNote,
]));
DB::table('action_logs')->where('id', $log->id)->update(['quantity' => (int) $quantityFromNote]);
}
});
});
}
return 0;
}
}

View File

@@ -3,8 +3,8 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class MoveUploadsToNewDisk extends Command
{
@@ -47,30 +47,29 @@ class MoveUploadsToNewDisk extends Command
}
$delete_local = $this->argument('delete_local');
$public_uploads['accessories'] = glob('public/uploads/accessories'."/*.*");
$public_uploads['assets'] = glob('public/uploads/assets'."/*.*");
$public_uploads['avatars'] = glob('public/uploads/avatars'."/*.*");
$public_uploads['categories'] = glob('public/uploads/categories'."/*.*");
$public_uploads['companies'] = glob('public/uploads/companies'."/*.*");
$public_uploads['components'] = glob('public/uploads/components'."/*.*");
$public_uploads['consumables'] = glob('public/uploads/consumables'."/*.*");
$public_uploads['departments'] = glob('public/uploads/departments'."/*.*");
$public_uploads['locations'] = glob('public/uploads/locations'."/*.*");
$public_uploads['manufacturers'] = glob('public/uploads/manufacturers'."/*.*");
$public_uploads['suppliers'] = glob('public/uploads/suppliers'."/*.*");
$public_uploads['assetmodels'] = glob('public/uploads/models'."/*.*");
$public_uploads['accessories'] = glob('public/uploads/accessories'.'/*.*');
$public_uploads['assets'] = glob('public/uploads/assets'.'/*.*');
$public_uploads['avatars'] = glob('public/uploads/avatars'.'/*.*');
$public_uploads['categories'] = glob('public/uploads/categories'.'/*.*');
$public_uploads['companies'] = glob('public/uploads/companies'.'/*.*');
$public_uploads['components'] = glob('public/uploads/components'.'/*.*');
$public_uploads['consumables'] = glob('public/uploads/consumables'.'/*.*');
$public_uploads['departments'] = glob('public/uploads/departments'.'/*.*');
$public_uploads['locations'] = glob('public/uploads/locations'.'/*.*');
$public_uploads['manufacturers'] = glob('public/uploads/manufacturers'.'/*.*');
$public_uploads['suppliers'] = glob('public/uploads/suppliers'.'/*.*');
$public_uploads['assetmodels'] = glob('public/uploads/models'.'/*.*');
// iterate files
foreach ($public_uploads as $public_type => $public_upload) {
$type_count = 0;
$this->info('- There are ' . count($public_upload) . ' PUBLIC ' . $public_type . ' files.');
$this->info('- There are '.count($public_upload).' PUBLIC '.$public_type.' files.');
for ($i = 0; $i < count($public_upload); $i++) {
$type_count++;
$filename = basename($public_upload[$i]);
try {
try {
Storage::disk('public')->put('uploads/'.$public_type.'/'.$filename, file_get_contents($public_upload[$i]));
$new_url = Storage::disk('public')->url('uploads/'.$public_type.'/'.$filename, $filename);
$this->info($type_count.'. PUBLIC: '.$filename.' was copied to '.$new_url);
@@ -81,49 +80,46 @@ class MoveUploadsToNewDisk extends Command
}
}
$logos = glob("public/uploads/setting*.*");
$this->info("- There are ".count($logos).' files that might be logos.');
$logos = glob('public/uploads/setting*.*');
$this->info('- There are '.count($logos).' files that might be logos.');
$type_count = 0;
foreach ($logos as $logo) {
$this->info($logo);
$type_count++;
$filename = basename($logo);
Storage::disk('public')->put('uploads/' . $filename, file_get_contents($logo));
$this->info($type_count . '. LOGO: ' . $filename . ' was copied to ' . env('PUBLIC_AWS_URL') . '/uploads/' . $filename);
Storage::disk('public')->put('uploads/'.$filename, file_get_contents($logo));
$this->info($type_count.'. LOGO: '.$filename.' was copied to '.env('PUBLIC_AWS_URL').'/uploads/'.$filename);
}
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
$private_uploads['assetmodels'] = glob('storage/private_uploads/models'."/*.*");
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");
$private_uploads['backups'] = glob('storage/private_uploads/backups'."/*.*");
$private_uploads['assets'] = glob('storage/private_uploads/assets'.'/*.*');
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'.'/*.*');
$private_uploads['audits'] = glob('storage/private_uploads/audits'.'/*.*');
$private_uploads['assetmodels'] = glob('storage/private_uploads/models'.'/*.*');
$private_uploads['imports'] = glob('storage/private_uploads/imports'.'/*.*');
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'.'/*.*');
$private_uploads['users'] = glob('storage/private_uploads/users'.'/*.*');
$private_uploads['backups'] = glob('storage/private_uploads/backups'.'/*.*');
foreach ($private_uploads as $private_type => $private_upload) {
{
$this->info('- There are ' . count($private_upload) . ' PRIVATE ' . $private_type . ' files.');
$type_count = 0;
for ($x = 0; $x < count($private_upload); $x++) {
$type_count++;
$filename = basename($private_upload[$x]);
$this->info('- There are '.count($private_upload).' PRIVATE '.$private_type.' files.');
try {
Storage::put($private_type . '/' . $filename, file_get_contents($private_upload[$i]));
$new_url = Storage::url($private_type . '/' . $filename, $filename);
$this->info($type_count . '. PRIVATE: ' . $filename . ' was copied to ' . $new_url);
} catch (\Exception $e) {
Log::debug($e);
$this->error($e);
}
$type_count = 0;
for ($x = 0; $x < count($private_upload); $x++) {
$type_count++;
$filename = basename($private_upload[$x]);
try {
Storage::put($private_type.'/'.$filename, file_get_contents($private_upload[$x]));
$new_url = Storage::url($private_type.'/'.$filename, $filename);
$this->info($type_count.'. PRIVATE: '.$filename.' was copied to '.$new_url);
} catch (\Exception $e) {
Log::debug($e);
$this->error($e);
}
}
if ($delete_local == 'true') {
$public_delete_count = 0;
$private_delete_count = 0;
@@ -160,7 +156,7 @@ class MoveUploadsToNewDisk extends Command
}
}
$this->info($public_delete_count . ' PUBLIC local files and ' . $private_delete_count . ' PRIVATE local files were deleted from your filesystem.');
$this->info($public_delete_count.' PUBLIC local files and '.$private_delete_count.' PRIVATE local files were deleted from your filesystem.');
}
}
}

View File

@@ -2,8 +2,8 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Console\Command;
class NormalizeUserNames extends Command
{
@@ -40,13 +40,13 @@ class NormalizeUserNames extends Command
{
$users = User::get();
$this->info($users->count() . ' users');
$this->info($users->count().' users');
foreach ($users as $user) {
$user->first_name = ucwords(strtolower($user->first_name));
$user->last_name = ucwords(strtolower($user->last_name));
$user->email = strtolower($user->email);
$user->save();
foreach ($users as $user) {
$user->first_name = ucwords(strtolower($user->first_name));
$user->last_name = ucwords(strtolower($user->last_name));
$user->email = strtolower($user->email);
$user->save();
}
}
}

View File

@@ -3,11 +3,10 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Console\Helper\ProgressIndicator;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
/**
* Class ObjectImportCommand
@@ -33,6 +32,11 @@ class ObjectImportCommand extends Command
*/
protected ProgressIndicator $progressIndicator;
/**
* Logger instance with configurable log path
*/
protected $logger;
/**
* Create a new command instance.
*
@@ -50,24 +54,26 @@ class ObjectImportCommand extends Command
*/
public function handle()
{
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); //600 seconds = 10 minutes
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); // 600 seconds = 10 minutes
ini_set('memory_limit', env('IMPORT_MEMORY_LIMIT', '500M'));
$this->progressIndicator = new ProgressIndicator($this->output);
$filename = $this->argument('filename');
$class = title_case($this->option('item-type'));
$class = ucfirst($this->option('item-type'));
$classString = "App\\Importer\\{$class}Importer";
$importer = new $classString($filename);
$importer->setCallbacks([$this, 'log'], [$this, 'progress'], [$this, 'errorCallback'])
->setCreatedBy($this->option('user_id'))
->setUpdating($this->option('update'))
->setShouldNotify($this->option('send-welcome'))
->setUsernameFormat($this->option('username_format'));
->setCreatedBy($this->option('user_id'))
->setUpdating($this->option('update'))
->setShouldNotify($this->option('send-welcome'))
->setUsernameFormat($this->option('username_format'));
$this->logger = Log::build([
'driver' => 'single',
'path' => $this->option('logfile'),
]);
// This $logFile/useFiles() bit is currently broken, so commenting it out for now
// $logFile = $this->option('logfile');
// Log::useFiles($logFile);
$this->progressIndicator->start('======= Importing Items from '.$filename.' =========');
$importer->import();
@@ -92,17 +98,19 @@ class ObjectImportCommand extends Command
* If a warning message is passed, we'll spit it to the console as well.
*
* @author Daniel Melzter
*
* @since 3.0
* @param string $string
* @param string $level
*
* @param string $string
* @param string $level
*/
public function log($string, $level = 'info')
{
if ($level === 'warning') {
Log::warning($string);
$this->logger->warning($string);
$this->comment($string);
} else {
Log::Info($string);
$this->logger->Info($string);
if ($this->option('verbose')) {
$this->comment($string);
}
@@ -113,7 +121,9 @@ class ObjectImportCommand extends Command
* Get the console command arguments.
*
* @author Daniel Melzter
*
* @since 3.0
*
* @return array
*/
protected function getArguments()
@@ -127,20 +137,22 @@ class ObjectImportCommand extends Command
* Get the console command options.
*
* @author Daniel Melzter
*
* @since 3.0
*
* @return array
*/
protected function getOptions()
{
return [
['email_format', null, InputOption::VALUE_REQUIRED, 'The format of the email addresses that should be generated. Options are firstname.lastname, firstname, filastname', null],
['username_format', null, InputOption::VALUE_REQUIRED, 'The format of the username that should be generated. Options are firstname.lastname, firstname, filastname, email', null],
['logfile', null, InputOption::VALUE_REQUIRED, 'The path to log output to. storage/logs/importer.log by default', storage_path('logs/importer.log')],
['item-type', null, InputOption::VALUE_REQUIRED, 'Item Type To import. Valid Options are Asset, Consumable, Accessory, License, or User', 'Asset'],
['web-importer', null, InputOption::VALUE_NONE, 'Internal: packages output for use with the web importer'],
['user_id', null, InputOption::VALUE_REQUIRED, 'ID of user creating items', 1],
['update', null, InputOption::VALUE_NONE, 'If a matching item is found, update item information'],
['send-welcome', null, InputOption::VALUE_NONE, 'Whether to send a welcome email to any new users that are created.'],
['email_format', null, InputOption::VALUE_REQUIRED, 'The format of the email addresses that should be generated. Options are firstname.lastname, firstname, filastname', null],
['username_format', null, InputOption::VALUE_REQUIRED, 'The format of the username that should be generated. Options are firstname.lastname, firstname, filastname, email', null],
['logfile', null, InputOption::VALUE_REQUIRED, 'The path to log output to. storage/logs/importer.log by default', storage_path('logs/importer.log')],
['item-type', null, InputOption::VALUE_REQUIRED, 'Item Type To import. Valid Options are Asset, Consumable, Accessory, License, or User', 'Asset'],
['web-importer', null, InputOption::VALUE_NONE, 'Internal: packages output for use with the web importer'],
['user_id', null, InputOption::VALUE_REQUIRED, 'ID of user creating items', 1],
['update', null, InputOption::VALUE_NONE, 'If a matching item is found, update item information'],
['send-welcome', null, InputOption::VALUE_NONE, 'Whether to send a welcome email to any new users that are created.'],
];
}
}

View File

@@ -2,11 +2,10 @@
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class PaveIt extends Command
{
@@ -42,9 +41,9 @@ class PaveIt extends Command
public function handle()
{
if (!$this->option('force')) {
if (! $this->option('force')) {
$confirmation = $this->confirm("\n****************************************************\nTHIS WILL DELETE ALL OF THE DATA IN YOUR DATABASE. \nThere is NO undo. This WILL destroy ALL of your data, \nINCLUDING ANY non-Snipe-IT tables you have in this database. \n****************************************************\n\nDo you wish to continue? No backsies! ");
if (!$confirmation) {
if (! $confirmation) {
$this->error('ABORTING');
exit(-1);
}
@@ -79,16 +78,16 @@ class PaveIt extends Command
foreach ($tables as $table_obj) {
$table = $table_obj['name'];
if (in_array($table, $except_tables)) {
$this->info($table. ' is SKIPPED.');
$this->info($table.' is SKIPPED.');
} else {
\DB::statement('truncate '.$table);
$this->info($table. ' is TRUNCATED.');
$this->info($table.' is TRUNCATED.');
}
}
// 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');
}
}
}

View File

@@ -149,13 +149,13 @@ class Purge extends Command
$filenames = Actionlog::where('action_type', 'uploaded')
->where('item_id', $user->id)
->pluck('filename');
foreach($filenames as $filename) {
foreach ($filenames as $filename) {
try {
if (Storage::exists($rel_path . '/' . $filename)) {
Storage::delete($rel_path . '/' . $filename);
if (Storage::exists($rel_path.'/'.$filename)) {
Storage::delete($rel_path.'/'.$filename);
}
} catch (\Exception $e) {
Log::info('An error occurred while deleting files: ' . $e->getMessage());
Log::info('An error occurred while deleting files: '.$e->getMessage());
}
}
$this->info('- User "'.$user->username.'" deleted.');

View File

@@ -0,0 +1,126 @@
<?php
namespace App\Console\Commands;
use App\Models\CheckoutAcceptance;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class PurgeEulaPDFs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:purge-eula-pdfs
{--older-than-days= : The number of days we should delete before }
{--force : Skip the interactive yes/no prompt for confirmation}
{--dryrun : Show the records that would be deleted but don\'t update the database or delete files from disk}
{--with-output : Display the results in a table in your console}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This purges signature files and EULAs from the system if they are older than the date passed with --older-than-days=.';
/**
* Execute the console command.
*/
public function handle()
{
$before = $this->option('older-than-days');
if (($before == '') || (! is_numeric($before))) {
return $this->error('ERROR: You must pass a valid number for --older-than-days (example: snipeit:purge-eula-pdfs --older-than-days=365.)');
}
$interval_date = Carbon::now()->subDays($before);
$signature_path = 'private_uploads/signatures/';
$eula_path = 'private_uploads/eula-pdfs/';
if (! Storage::exists($eula_path)) {
$this->fail('The storage directory "'.$eula_path.'" does not exist. No EULA files will be deleted.');
}
if (! Storage::exists($signature_path)) {
$this->fail('The storage directory "'.$signature_path.'" does not exist. No signature files will be deleted.');
}
if ($this->option('dryrun')) {
$this->info('This script is being run with the --dryrun option. No files or records will be deleted.');
}
$acceptances = CheckoutAcceptance::HasFiles()->where('updated_at', '<', $interval_date)->with('assignedTo')->get();
if (! $this->option('force')) {
if ($this->confirm("\n****************************************************\nTHIS WILL DELETE ALL OF THE SIGNATURES AND EULA PDF FILES SINCE $interval_date. \nThere is NO undo! \n****************************************************\n\nDo you wish to continue? No backsies! [y|N]")) {
}
}
if ($acceptances->count() == 0) {
return $this->warn('There are no item acceptances with signatures or EULA PDFs from before '.$interval_date);
}
$this->info(number_format($acceptances->count()).' EULA PDFs from before '.$interval_date.' will be purged');
if (! $this->option('with-output')) {
$this->info('Run this command with the --with-output option to see the full list in the console.');
} else {
$this->table(
[
trans('general.user'),
trans('general.type'),
trans('general.item'),
trans('general.category'),
trans('general.accepted_date'),
trans('general.declined_date'),
trans('general.signature'),
trans('general.filename'),
],
$acceptances->map(fn ($acceptance) => [
trans('general.user') => $acceptance->assignedTo->display_name,
trans('general.type') => $acceptance->display_checkoutable_type,
trans('general.item') => $acceptance->checkoutable_type::find($acceptance->checkoutable_id)->display_name,
trans('general.category') => $acceptance->checkoutable_category_name,
trans('general.accepted_date') => $acceptance->accepted_at,
trans('general.declined_date') => $acceptance->declined_at,
trans('general.signature') => $acceptance->signature_filename,
trans('general.filename') => $acceptance->stored_eula_file,
])
);
}
foreach ($acceptances as $acceptance) {
$signature_file = $signature_path.$acceptance->signature_filename;
$eula_file = $eula_path.$acceptance->stored_eula_file;
if (Storage::exists($signature_file)) {
if (! $this->option('dryrun')) {
Storage::delete($signature_file);
}
} else {
$this->error('The file "'.$signature_file.'" does not exist.');
}
if (Storage::exists($eula_file)) {
if (! $this->option('dryrun')) {
Storage::delete($eula_file);
}
} else {
$this->error('The file "'.$eula_file.'" does not exist.');
}
if (! $this->option('dryrun')) {
$acceptance->delete();
}
}
}
}

View File

@@ -82,10 +82,10 @@ class ReEncodeCustomFieldNames extends Command
if ($field->db_column == $field->convertUnicodeDbSlug() && \Schema::hasColumn('assets', $field->convertUnicodeDbSlug())) {
$this->info('-- ✓ This field exists on the assets table and the value for db_column matches in the custom_fields table.');
/**
* There is a mismatch between the fieldname on the assets table and
* what $field->convertUnicodeDbSlug() is *now* expecting.
*/
/**
* There is a mismatch between the fieldname on the assets table and
* what $field->convertUnicodeDbSlug() is *now* expecting.
*/
} else {
if ($field->db_column != $field->convertUnicodeDbSlug()) {
@@ -96,7 +96,6 @@ class ReEncodeCustomFieldNames extends Command
}
/** Make sure the custom_field_columns array has the ID */
if (array_key_exists($field->id, $custom_field_columns)) {
@@ -114,7 +113,6 @@ class ReEncodeCustomFieldNames extends Command
$field->db_column = $field->convertUnicodeDbSlug();
$field->save();
} else {
$this->warn('-- ✘ WARNING: There is no field on the assets table ending in '.$field->id.'. This may require more in-depth investigation and may mean the schema was altered manually.');
}

View File

@@ -4,8 +4,8 @@ namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\Setting;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class RegenerateAssetTags extends Command
{

View File

@@ -44,7 +44,7 @@ class RemoveExplicitEols extends Command
}
$endTime = microtime(true);
$executionTime = ($endTime - $startTime);
$this->info('Command executed in ' . round($executionTime, 2) . ' seconds.');
$this->info('Command executed in '.round($executionTime, 2).' seconds.');
}
private function updateAssets($assets)
@@ -55,6 +55,6 @@ class RemoveExplicitEols extends Command
$asset->save();
}
$this->info($assets->count() . ' Assets updated successfully');
$this->info($assets->count().' Assets updated successfully');
}
}

View File

@@ -38,7 +38,7 @@ class RemoveInvalidUploadDeleteActionLogItems extends Command
return 0;
}
$this->table(['ID', 'Action Type', 'Item Type', 'Item ID', 'Created At', 'Deleted At'], $invalidLogs->map(fn($log) => [
$this->table(['ID', 'Action Type', 'Item Type', 'Item ID', 'Created At', 'Deleted At'], $invalidLogs->map(fn ($log) => [
$log->id,
$log->action_type,
$log->item_type,
@@ -48,7 +48,7 @@ class RemoveInvalidUploadDeleteActionLogItems extends Command
])->toArray());
if ($this->confirm("Do you wish to remove {$invalidLogs->count()} log items?")) {
$invalidLogs->each(fn($log) => $log->forceDelete());
$invalidLogs->each(fn ($log) => $log->forceDelete());
}
return 0;

View File

@@ -2,7 +2,6 @@
namespace App\Console\Commands;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Console\Command;
@@ -49,14 +48,16 @@ class ResetDemoSettings extends Command
$settings->logo = 'snipe-logo.png';
$settings->alert_email = 'service@snipe-it.io';
$settings->login_note = 'Use `admin` / `password` to login to the demo.';
$settings->header_color = null;
$settings->header_color = '#3c8dbc';
$settings->link_dark_color = '#5fa4cc';
$settings->link_light_color = '#296282;';
$settings->nav_link_color = '#FFFFFF';
$settings->label2_2d_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->skin = '';
$settings->email_domain = 'snipeitapp.com';
$settings->email_format = 'filastname';
$settings->username_format = 'filastname';
@@ -75,11 +76,12 @@ class ResetDemoSettings extends Command
$settings->saml_custom_settings = null;
$settings->default_avatar = 'default.png';
$settings->save();
if ($user = User::where('username', '=', 'admin')->first()) {
$user->locale = 'en-US';
$user->enable_confetti = 1;
$user->enable_sounds = 1;
$user->save();
}
@@ -87,5 +89,4 @@ class ResetDemoSettings extends Command
\Storage::disk('public')->put('snipe-logo-lg.png', file_get_contents(public_path('img/demo/snipe-logo-lg.png')));
}
}

View File

@@ -6,9 +6,9 @@ use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\License;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
class RestoreDeletedUsers extends Command
{
@@ -75,7 +75,7 @@ class RestoreDeletedUsers extends Command
DB::table('assets')
->where('id', $user_log->item_id)
->update(['assigned_to' => $user->id, 'assigned_type'=> User::class]);
->update(['assigned_to' => $user->id, 'assigned_type' => User::class]);
$this->info(' ** Asset '.$user_log->item->id.' ('.$user_log->item->asset_tag.') restored to user '.$user->id.'');
} elseif ($user_log->item_type == License::class) {

View File

@@ -2,14 +2,17 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use ZipArchive;
use Illuminate\Support\Facades\Log;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use ZipArchive;
class SQLStreamer {
class SQLStreamer
{
private $input;
private $output;
// embed the prefix here?
public ?string $prefix;
@@ -18,106 +21,112 @@ class SQLStreamer {
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
public array $tablenames = [];
private bool $should_guess = false;
private bool $statement_is_permitted = false;
public function __construct($input, $output, string $prefix = null)
public function __construct($input, $output, ?string $prefix = null)
{
$this->input = $input;
$this->output = $output;
$this->prefix = $prefix;
}
public function parse_sql(string $line): string {
public function parse_sql(string $line): string
{
// take into account the 'start of line or not' setting as an instance variable?
// 'continuation' lines for a permitted statement are PERMITTED.
// remove *only* line-feeds & carriage-returns; helpful for regexes against lines from
// Windows dumps
$line = trim($line, "\r\n");
if($this->statement_is_permitted && $line[0] === ' ') {
return $line . "\n"; //re-add the newline
if ($this->statement_is_permitted && $line[0] === ' ') {
return $line."\n"; // re-add the newline
}
$table_regex = '`?([a-zA-Z0-9_]+)`?';
$allowed_statements = [
"/^(DROP TABLE (?:IF EXISTS )?)`$table_regex(.*)$/" => false,
"/^(CREATE TABLE )$table_regex(.*)$/" => true, //sets up 'continuation'
"/^(CREATE TABLE )$table_regex(.*)$/" => true, // sets up 'continuation'
"/^(LOCK TABLES )$table_regex(.*)$/" => false,
"/^(INSERT INTO )$table_regex(.*)$/" => false,
"/^UNLOCK TABLES/" => false,
'/^UNLOCK TABLES/' => false,
// "/^\\) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;/" => false, // FIXME not sure what to do here?
"/^\\)[a-zA-Z0-9_= ]*;$/" => false,
'/^\\)[a-zA-Z0-9_= ]*;$/' => false,
// ^^^^^^ that bit should *exit* the 'permitted' block
"/^\\(.*\\)[,;]$/" => false, //older MySQL dump style with one set of values per line
'/^\\(.*\\)[,;]$/' => false, // older MySQL dump style with one set of values per line
/* 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
"<^/\*![0-9]{5} 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) {
// $this->info("Checking regex: $statement...\n");
foreach ($allowed_statements as $statement => $statechange) {
// $this->info("Checking regex: $statement...\n");
$matches = [];
if (preg_match($statement,$line,$matches)) {
if (preg_match($statement, $line, $matches)) {
$this->statement_is_permitted = $statechange;
// matches are: 1 => first part of the statement, 2 => tablename, 3 => rest of statement
// (with of course 0 being "the whole match")
if (@$matches[2]) {
// print "Found a tablename! It's: ".$matches[2]."\n";
// print "Found a tablename! It's: ".$matches[2]."\n";
if ($this->should_guess) {
@$this->tablenames[$matches[2]] += 1;
continue; //oh? FIXME
continue; // oh? FIXME
} else {
$cleaned_tablename = \DB::getTablePrefix().preg_replace('/^'.$this->prefix.'/','',$matches[2]);
$line = preg_replace($statement,'$1`'.$cleaned_tablename.'`$3' , $line);
$cleaned_tablename = \DB::getTablePrefix().preg_replace('/^'.$this->prefix.'/', '', $matches[2]);
$line = preg_replace($statement, '$1`'.$cleaned_tablename.'`$3', $line);
}
} else {
// no explicit tablename in this one, leave the line alone
}
//how do we *replace* the tablename?
// print "RETURNING LINE: $line";
return $line . "\n"; //re-add newline
// how do we *replace* the tablename?
// print "RETURNING LINE: $line";
return $line."\n"; // re-add newline
}
}
// all that is not allowed is denied.
return "";
return '';
}
//this is used in exactly *TWO* places, and in both cases should return a prefix I think?
// this is used in exactly *TWO* places, and in both cases should return a prefix I think?
// first - if you do the --sanitize-only one (which is mostly for testing/development)
// next - when you run *without* a guessed prefix, this is run first to figure out the prefix
// I think we have to *duplicate* the call to be able to run it again?
public static function guess_prefix($input):string
public static function guess_prefix($input): string
{
$parser = new self($input, null);
$parser->should_guess = true;
$parser->line_aware_piping(); // <----- THIS is doing the heavy lifting!
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics?
//can't use 'users' because the 'accessories_checkout' table?
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; // TODO - move to statics?
// can't use 'users' because the 'accessories_checkout' table?
// can't use 'assets' because 'ver1_components_assets'
foreach($check_tables as $check_table => $_ignore) {
foreach ($check_tables as $check_table => $_ignore) {
foreach ($parser->tablenames as $tablename => $_count) {
// print "Comparing $tablename to $check_table\n";
if (str_ends_with($tablename,$check_table)) {
// print "Found one!\n";
$check_tables[$check_table] = substr($tablename,0,-strlen($check_table));
// print "Comparing $tablename to $check_table\n";
if (str_ends_with($tablename, $check_table)) {
// print "Found one!\n";
$check_tables[$check_table] = substr($tablename, 0, -strlen($check_table));
}
}
}
$guessed_prefix = null;
foreach ($check_tables as $clean_table => $prefix_guess) {
if(is_null($prefix_guess)) {
print("Couldn't find table $clean_table\n");
die();
if (is_null($prefix_guess)) {
echo "Couldn't find table $clean_table\n";
exit();
}
if(is_null($guessed_prefix)) {
if (is_null($guessed_prefix)) {
$guessed_prefix = $prefix_guess;
} else {
if ($guessed_prefix != $prefix_guess) {
print("Prefix mismatch! Had guessed $guessed_prefix but got $prefix_guess\n");
die();
echo "Prefix mismatch! Had guessed $guessed_prefix but got $prefix_guess\n";
exit();
}
}
}
@@ -130,7 +139,7 @@ class SQLStreamer {
{
$bytes_read = 0;
if (! $this->input) {
throw new \Exception("No Input available for line_aware_piping");
throw new \Exception('No Input available for line_aware_piping');
}
while (($buffer = fgets($this->input, SQLStreamer::$buffer_size)) !== false) {
@@ -142,25 +151,24 @@ class SQLStreamer {
$bytes_written = fwrite($this->output, $cleaned_buffer);
if ($bytes_written === false) {
throw new \Exception("Unable to write to pipe");
throw new \Exception('Unable to write to pipe');
}
}
}
// if we got a newline at the end of this, then the _next_ read is the beginning of a line
if($buffer[strlen($buffer)-1] === "\n") {
if ($buffer[strlen($buffer) - 1] === "\n") {
$this->reading_beginning_of_line = true;
} else {
$this->reading_beginning_of_line = false;
}
}
return $bytes_read;
}
}
class RestoreFromBackup extends Command
{
/**
@@ -202,7 +210,7 @@ class RestoreFromBackup extends Command
public function handle()
{
$dir = getcwd();
if( $dir != base_path() ) { // usually only the case when running via webserver, not via command-line
if ($dir != base_path()) { // usually only the case when running via webserver, not via command-line
Log::debug("Current working directory is: $dir, changing directory to: ".base_path());
chdir(base_path()); // TODO - is this *safe* to change on a running script?!
}
@@ -221,7 +229,7 @@ class RestoreFromBackup extends Command
return $this->error('DB_CONNECTION must be MySQL in order to perform a restore. Detected: '.config('database.default'));
}
$za = new ZipArchive();
$za = new ZipArchive;
$errcode = $za->open($filename/* , ZipArchive::RDONLY */); // that constant only exists in PHP 7.4 and higher
if ($errcode !== true) {
@@ -240,13 +248,12 @@ class RestoreFromBackup extends Command
return $this->error('Could not access file: '.$filename.' - '.array_key_exists($errcode, $errors) ? $errors[$errcode] : " Unknown reason: $errcode");
}
$private_dirs = [
'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels' => 'storage/private_uploads/models', //this was changed from assetmodels => models Aug 10 2025
'storage/private_uploads/asset_maintenances' => 'storage/private_uploads/maintenances', //this was changed from asset_maintenances => maintenances Aug 10 2025
'storage/private_uploads/maintenances', //but let 'maintenances' take precedence
'storage/private_uploads/models', //and let 'models' take precedence
'storage/private_uploads/assetmodels' => 'storage/private_uploads/models', // this was changed from assetmodels => models Aug 10 2025
'storage/private_uploads/asset_maintenances' => 'storage/private_uploads/maintenances', // this was changed from asset_maintenances => maintenances Aug 10 2025
'storage/private_uploads/maintenances', // but let 'maintenances' take precedence
'storage/private_uploads/models', // and let 'models' take precedence
'storage/private_uploads/assets', // these are asset _files_, not the pictures.
'storage/private_uploads/audits',
'storage/private_uploads/components',
@@ -297,10 +304,10 @@ class RestoreFromBackup extends Command
$good_extensions = config('filesystems.allowed_upload_extensions_array');
$private_extensions = array_merge($good_extensions, ["csv", "key"]); //add csv, and 'key'
$public_extensions = array_diff($good_extensions, ["xml"]); //remove xml
$private_extensions = array_merge($good_extensions, ['csv', 'key']); // add csv, and 'key'
$public_extensions = array_diff($good_extensions, ['xml']); // remove xml
$sanitizer = new Sanitizer();
$sanitizer = new Sanitizer;
/**
* TODO: I _hate_ the "continue 3" thing we keep doing here
@@ -315,29 +322,30 @@ class RestoreFromBackup extends Command
// print_r($stat_results);
$raw_path = $stat_results['name'];
if (strpos($raw_path, '\\') !== false) { //found a backslash, swap it to forward-slash
if (strpos($raw_path, '\\') !== false) { // found a backslash, swap it to forward-slash
$raw_path = strtr($raw_path, '\\', '/');
//print "Translating file: ".$stat_results['name']." to: ".$raw_path."\n";
// print "Translating file: ".$stat_results['name']." to: ".$raw_path."\n";
}
// skip macOS resource fork files (?!?!?!)
if (strpos($raw_path, '__MACOSX') !== false && strpos($raw_path, '._') !== false) {
//print "SKIPPING macOS Resource fork file: $raw_path\n";
// print "SKIPPING macOS Resource fork file: $raw_path\n";
// $boring_files[] = $raw_path; //stop adding this to the boring files list; it's just confusing
continue;
}
if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') {
Log::debug("Found a sql file!");
Log::debug('Found a sql file!');
$sqlfiles[] = $raw_path;
$sqlfile_indices[] = $i;
continue;
}
if ($raw_path[-1] == '/') {
//last character is '/' - this is a directory, and we don't need it, and we don't need to warn about it
// last character is '/' - this is a directory, and we don't need it, and we don't need to warn about it
continue;
}
if (in_array(basename($raw_path), [".gitkeep", ".gitignore", ".DS_Store"])) {
//skip these boring files silently without reporting on them; they're stupid
if (in_array(basename($raw_path), ['.gitkeep', '.gitignore', '.DS_Store'])) {
// skip these boring files silently without reporting on them; they're stupid
continue;
}
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
@@ -351,20 +359,21 @@ class RestoreFromBackup extends Command
if (is_int($dir)) {
$dir = $destdir;
}
$last_pos = strrpos($raw_path, $dir . '/');
$last_pos = strrpos($raw_path, $dir.'/');
if ($last_pos !== false) {
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
//the CSV bit, below, is because we store CSV files as "blahcsv" - without an extension
if (!in_array($extension, $allowed_extensions) && !($dir == "storage/private_uploads/imports" && substr($raw_path, -3) == "csv" && $extension == "")) {
// print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
// print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
// the CSV bit, below, is because we store CSV files as "blahcsv" - without an extension
if (! in_array($extension, $allowed_extensions) && ! ($dir == 'storage/private_uploads/imports' && substr($raw_path, -3) == 'csv' && $extension == '')) {
$unsafe_files[] = $raw_path;
Log::debug($raw_path . ' from directory ' . $dir . ' is being skipped');
Log::debug($raw_path.' from directory '.$dir.' is being skipped');
} else {
if ($dir != $destdir) {
Log::debug("Getting ready to save file $raw_path to new directory $destdir");
}
$interesting_files[$raw_path] = ['dest' => $destdir, 'index' => $i];
}
continue 3;
}
}
@@ -378,28 +387,30 @@ class RestoreFromBackup extends Command
foreach ($files as $file) {
$has_wildcard = (strpos($file, '*') !== false);
if ($has_wildcard) {
$file = substr($file, 0, -1); //trim last character (which should be the wildcard)
$file = substr($file, 0, -1); // trim last character (which should be the wildcard)
}
$last_pos = strrpos($raw_path, $file); // no trailing slash!
if ($last_pos !== false) {
if (!in_array($extension, $allowed_extensions)) {
if (! in_array($extension, $allowed_extensions)) {
// gathering potentially unsafe files here to return at exit
$unsafe_files[] = $raw_path;
Log::debug('Potentially unsafe file ' . $raw_path . ' is being skipped');
Log::debug('Potentially unsafe file '.$raw_path.' is being skipped');
$boring_files[] = $raw_path;
continue 3;
}
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $file - last_pos+strlen(\$file) is: ".($last_pos+strlen($file))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//no wildcards found in $file, process 'normally'
if ($last_pos + strlen($file) == strlen($raw_path) || $has_wildcard) { //again, no trailing slash. or this is a wildcard and we just take it.
// print("INTERESTING - last_pos is $last_pos when searching $raw_path for $file - last_pos+strlen(\$file) is: ".($last_pos+strlen($file))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
// no wildcards found in $file, process 'normally'
if ($last_pos + strlen($file) == strlen($raw_path) || $has_wildcard) { // again, no trailing slash. or this is a wildcard and we just take it.
// print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
$interesting_files[$raw_path] = ['dest' => dirname($file), 'index' => $i];
continue 3;
}
}
}
}
$boring_files[] = $raw_path; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file
$boring_files[] = $raw_path; // if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file
} // end of pre-processing the ZIP file for-loop
// print_r($interesting_files);exit(-1);
@@ -408,12 +419,12 @@ class RestoreFromBackup extends Command
}
if (strpos($sqlfiles[0], 'db-dumps') === false) {
//return $this->error("SQL backup file is missing 'db-dumps' component of full pathname: ".$sqlfiles[0]);
//older Snipe-IT installs don't have the db-dumps subdirectory component
// return $this->error("SQL backup file is missing 'db-dumps' component of full pathname: ".$sqlfiles[0]);
// older Snipe-IT installs don't have the db-dumps subdirectory component
}
$sql_stat = $za->statIndex($sqlfile_indices[0]);
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
// $this->info("SQL Stat is: ".print_r($sql_stat,true));
$sql_contents = $za->getStream($sql_stat['name']); // maybe copy *THIS* thing?
// OKAY, now that we *found* the sql file if we're doing just the guess-prefix thing, we can do that *HERE* I think?
@@ -428,27 +439,28 @@ class RestoreFromBackup extends Command
if ($this->option('sql-stdout-only')) {
$sql_importer = new SQLStreamer($sql_contents, STDOUT, $this->option('sanitize-with-prefix'));
$bytes_read = $sql_importer->line_aware_piping();
return $this->warn("$bytes_read total bytes read");
//TODO - it'd be nice to dump this message to STDERR so that STDOUT is just pure SQL,
// TODO - it'd be nice to dump this message to STDERR so that STDOUT is just pure SQL,
// which would be good for redirecting to a file, and not having to trim the last line off of it
}
//how to invoke the restore?
// how to invoke the restore?
$pipes = [];
$env_vars = getenv();
$env_vars['MYSQL_PWD'] = config('database.connections.mysql.password');
// TODO notes: we are stealing the dump_binary_path (which *probably* also has your copy of the mysql binary in it. But it might not, so we might need to extend this)
// we unilaterally prepend a slash to the `mysql` command. This might mean your path could look like /blah/blah/blah//mysql - which should be fine. But maybe in some environments it isn't?
$mysql_binary = config('database.connections.mysql.dump.dump_binary_path').\DIRECTORY_SEPARATOR.'mysql'.(\DIRECTORY_SEPARATOR == '\\' ? ".exe" : "");
if( ! file_exists($mysql_binary) ) {
$mysql_binary = config('database.connections.mysql.dump.dump_binary_path').\DIRECTORY_SEPARATOR.'mysql'.(\DIRECTORY_SEPARATOR == '\\' ? '.exe' : '');
if (! file_exists($mysql_binary)) {
return $this->error("mysql tool at: '$mysql_binary' does not exist, cannot restore. Please edit DB_DUMP_PATH in your .env to point to a directory that contains the mysqldump and mysql binary");
}
$proc_results = proc_open("$mysql_binary -h ".escapeshellarg(config('database.connections.mysql.host')).' -u '.escapeshellarg(config('database.connections.mysql.username')).' '.escapeshellarg(config('database.connections.mysql.database')), // yanked -p since we pass via ENV
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
null,
$env_vars); // this is not super-duper awesome-secure, but definitely more secure than showing it on the CLI, or dropping temporary files with passwords in them.
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
null,
$env_vars); // this is not super-duper awesome-secure, but definitely more secure than showing it on the CLI, or dropping temporary files with passwords in them.
if ($proc_results === false) {
return $this->error('Unable to invoke mysql via CLI');
}
@@ -462,7 +474,7 @@ class RestoreFromBackup extends Command
// should we read stdout?
// fwrite($pipes[0],config("database.connections.mysql.password")."\n"); //this doesn't work :(
//$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy!
// $sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy!
// FIXME - this feels like it wants to go somewhere else?
// and it doesn't seem 'right' - if you can't get a stream to the .sql file,
@@ -477,7 +489,7 @@ class RestoreFromBackup extends Command
}
try {
if ( $this->option('sanitize-with-prefix') === null) {
if ($this->option('sanitize-with-prefix') === null) {
// "Legacy" direct-piping
$bytes_read = 0;
while (($buffer = fgets($sql_contents, SQLStreamer::$buffer_size)) !== false) {
@@ -486,7 +498,7 @@ class RestoreFromBackup extends Command
$bytes_written = fwrite($pipes[0], $buffer);
if ($bytes_written === false) {
throw new Exception("Unable to write to pipe");
throw new Exception('Unable to write to pipe');
}
}
} else {
@@ -494,37 +506,37 @@ class RestoreFromBackup extends Command
$bytes_read = $sql_importer->line_aware_piping();
}
} catch (\Exception $e) {
Log::error("Error during restore!!!! ".$e->getMessage());
Log::error('Error during restore!!!! '.$e->getMessage());
// FIXME - put these back and/or put them in the right places?!
$err_out = fgets($pipes[1]);
$err_err = fgets($pipes[2]);
Log::error("Error OUTPUT: ".$err_out);
Log::error('Error OUTPUT: '.$err_out);
$this->info($err_out);
Log::error("Error ERROR : ".$err_err);
Log::error('Error ERROR : '.$err_err);
$this->error($err_err);
throw $e;
}
if (!feof($sql_contents) || $bytes_read == 0) {
return $this->error("Not at end of file for sql file, or zero bytes read. aborting!");
if (! feof($sql_contents) || $bytes_read == 0) {
return $this->error('Not at end of file for sql file, or zero bytes read. aborting!');
}
fclose($pipes[0]);
fclose($sql_contents);
$this->line(stream_get_contents($pipes[1]));
fclose($pipes[1]);
$this->error(stream_get_contents($pipes[2]));
fclose($pipes[2]);
//wait, have to do fclose() on all pipes first?
// wait, have to do fclose() on all pipes first?
$close_results = proc_close($proc_results);
if ($close_results != 0) {
return $this->error('There may have been a problem with the database import: Error number '.$close_results);
}
//and now copy the files over too (right?)
//FIXME - we don't prune the filesystem space yet!!!!
// and now copy the files over too (right?)
// FIXME - we don't prune the filesystem space yet!!!!
if ($this->option('no-progress')) {
$bar = null;
} else {
@@ -532,16 +544,16 @@ class RestoreFromBackup extends Command
}
foreach ($interesting_files as $pretty_file_name => $file_details) {
$ugly_file_name = $za->statIndex($file_details['index'])['name'];
$migrated_file_name = $file_details['dest'] . '/' . basename($pretty_file_name);
if (strcasecmp(substr($pretty_file_name, -4), ".svg") === 0) {
$migrated_file_name = $file_details['dest'].'/'.basename($pretty_file_name);
if (strcasecmp(substr($pretty_file_name, -4), '.svg') === 0) {
$svg_contents = $za->getFromIndex($file_details['index']);
$cleaned_svg = $sanitizer->sanitize($svg_contents);
file_put_contents($migrated_file_name, $cleaned_svg);
} else {
$fp = $za->getStream($ugly_file_name);
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
if (!is_dir($file_details['dest'])) {
mkdir($file_details['dest'], 0755, true); //0755 is what Laravel uses, so we do that
// $this->info("Weird problem, here are file details? ".print_r($file_details,true));
if (! is_dir($file_details['dest'])) {
mkdir($file_details['dest'], 0755, true); // 0755 is what Laravel uses, so we do that
}
$migrated_file = fopen($migrated_file_name, 'w');
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
@@ -549,7 +561,7 @@ class RestoreFromBackup extends Command
}
fclose($migrated_file);
fclose($fp);
//$this->info("Wrote $ugly_file_name to $pretty_file_name");
// $this->info("Wrote $ugly_file_name to $pretty_file_name");
}
if ($bar) {
$bar->advance();

View File

@@ -5,10 +5,10 @@ namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Setting;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Console\Command;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Facades\Artisan;
class RotateAppKey extends Command
{
@@ -46,12 +46,13 @@ class RotateAppKey extends Command
*/
public function handle()
{
//make sure they specify only exactly one of --emergency, or a filename. Not neither, and not both.
if ( (!$this->option('emergency') && !$this->argument('previous_key')) || ( $this->option('emergency') && $this->argument('previous_key'))) {
$this->error("Specify only one of --emergency, or an app key value, in order to rotate keys");
// make sure they specify only exactly one of --emergency, or a filename. Not neither, and not both.
if ((! $this->option('emergency') && ! $this->argument('previous_key')) || ($this->option('emergency') && $this->argument('previous_key'))) {
$this->error('Specify only one of --emergency, or an app key value, in order to rotate keys');
return 1;
}
if ( $this->option('emergency') ) {
if ($this->option('emergency')) {
$msg = "\n****************************************************\nTHIS WILL MODIFY YOUR APP_KEY AND DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND \nRE-ENCRYPT THEM WITH A NEWLY GENERATED KEY. \n\nThere is NO undo. \n\nMake SURE you have a database backup and a backup of your .env generated BEFORE running this command. \n\nIf you do not save the newly generated APP_KEY to your .env in this process, \nyour encrypted data will no longer be decryptable. \n\nAre you SURE you wish to continue, and have confirmed you have a database backup and an .env backup? ";
} else {
$msg = "\n****************************************************\nTHIS WILL DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND RE-ENCRYPT THEM WITH YOUR\nAPP_KEY.\n\nThere is NO undo. \n\nMake SURE you have a database backup BEFORE running this command. \n\nAre you SURE you wish to continue, and have confirmed you have a database backup? ";
@@ -79,9 +80,9 @@ class RotateAppKey extends Command
$new_app_key = config('app.key');
}
$this->warn('Your app cipher is: ' . $cipher);
$this->warn('Your old APP_KEY is: ' . $old_app_key);
$this->warn('Your new APP_KEY is: ' . $new_app_key);
$this->warn('Your app cipher is: '.$cipher);
$this->warn('Your old APP_KEY is: '.$old_app_key);
$this->warn('Your new APP_KEY is: '.$new_app_key);
// Manually create an old encrypter instance using the old app key
// and also create a new encrypter instance so we can re-crypt the field
@@ -97,12 +98,13 @@ class RotateAppKey extends Command
foreach ($assets as $asset) {
try {
$asset->{$field->db_column} = $oldEncrypter->decrypt($asset->{$field->db_column});
$this->line('DECRYPTED: ' . $field->db_column);
$this->line('DECRYPTED: '.$field->db_column);
} catch (DecryptException $e) {
$this->line('Could not decrypt '. $field->db_column.' using "old key" - skipping...');
$this->line('Could not decrypt '.$field->db_column.' using "old key" - skipping...');
continue;
} catch (\Exception $e) {
$this->error("Error decrypting ".$field->db_column.", reason: ".$e->getMessage().". Aborting key rotation");
$this->error('Error decrypting '.$field->db_column.', reason: '.$e->getMessage().'. Aborting key rotation');
throw $e;
}
$asset->{$field->db_column} = $newEncrypter->encrypt($asset->{$field->db_column});
@@ -119,8 +121,8 @@ class RotateAppKey extends Command
$setting->ldap_pword = $newEncrypter->encrypt($setting->ldap_pword);
$setting->save();
$this->warn('LDAP password has been re-encrypted.');
} catch(DecryptException $e) {
$this->warn("Unable to decrypt old LDAP password; skipping");
} catch (DecryptException $e) {
$this->warn('Unable to decrypt old LDAP password; skipping');
}
}
} else {

View File

@@ -2,8 +2,8 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\SamlNonce;
use Illuminate\Console\Command;
class SamlClearExpiredNonces extends Command
{
@@ -38,7 +38,8 @@ class SamlClearExpiredNonces extends Command
*/
public function handle()
{
SamlNonce::where('not_valid_after','<=',now())->delete();
SamlNonce::where('not_valid_after', '<=', now())->delete();
return 0;
}
}

View File

@@ -3,13 +3,15 @@
namespace App\Console\Commands;
use App\Mail\UnacceptedAssetReminderMail;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\Setting;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\LicenseSeat;
use App\Models\User;
use App\Notifications\CheckoutAssetNotification;
use App\Notifications\CurrentInventory;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Facades\Mail;
class SendAcceptanceReminder extends Command
@@ -26,7 +28,7 @@ class SendAcceptanceReminder extends Command
*
* @var string
*/
protected $description = 'This will resend users with unaccepted assets a reminder to accept or decline them.';
protected $description = 'This will resend users with unaccepted items a reminder to accept or decline them.';
/**
* Create a new command instance.
@@ -45,28 +47,39 @@ class SendAcceptanceReminder extends Command
*/
public function handle()
{
$pending = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset')
->whereHas('checkoutable', function($query) {
$query->where('accepted_at', null)
->where('declined_at', null);
})
->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model', 'checkoutable.adminuser'])
->get();
$pending = CheckoutAcceptance::query()
->with([
'checkoutable' => function (MorphTo $morph) {
$morph->morphWith([
Asset::class => ['model.category', 'assignedTo', 'adminuser', 'company', 'checkouts'],
Accessory::class => ['category', 'company', 'checkouts'],
LicenseSeat::class => ['user', 'license', 'checkouts'],
Component::class => ['assignedTo', 'company', 'checkouts'],
Consumable::class => ['company', 'checkouts'],
]);
},
'assignedTo',
])
->whereHasMorph(
'checkoutable',
[Asset::class, Accessory::class, LicenseSeat::class, Component::class, Consumable::class],
fn ($q) => $q->whereNull('accepted_at')
->whereNull('declined_at')
)
->pending()
->get();
$count = 0;
$unacceptedAssetGroups = $pending
->filter(function($acceptance) {
return $acceptance->checkoutable_type == 'App\Models\Asset';
})
->map(function($acceptance) {
->map(function ($acceptance) {
return ['assetItem' => $acceptance->checkoutable, 'acceptance' => $acceptance];
})
->groupBy(function($item) {
->groupBy(function ($item) {
return $item['acceptance']->assignedTo ? $item['acceptance']->assignedTo->id : '';
});
$no_email_list= [];
$no_email_list = [];
foreach($unacceptedAssetGroups as $unacceptedAssetGroup) {
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'];
@@ -74,7 +87,7 @@ class SendAcceptanceReminder extends Command
$locale = $acceptance->assignedTo?->locale;
$email = $acceptance->assignedTo?->email;
if(!$email){
if (! $email) {
$no_email_list[] = [
'id' => $acceptance->assignedTo?->id,
'name' => $acceptance->assignedTo?->display_name,
@@ -100,12 +113,11 @@ class SendAcceptanceReminder extends Command
$rows[] = [$user['id'], $user['name']];
}
if (!empty($rows)) {
$this->info("The following users do not have an email address:");
if (! empty($rows)) {
$this->info('The following users do not have an email address:');
$this->table($headers, $rows);
}
return 0;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Helpers\Helper;
use App\Models\Asset;
use App\Models\Recipients\AlertRecipient;
use App\Models\Setting;
@@ -9,6 +10,7 @@ use App\Notifications\ExpectedCheckinAdminNotification;
use App\Notifications\ExpectedCheckinNotification;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Notification;
class SendExpectedCheckinAlerts extends Command
{
@@ -17,7 +19,7 @@ class SendExpectedCheckinAlerts extends Command
*
* @var string
*/
protected $name = 'snipeit:expected-checkin';
protected $signature = 'snipeit:expected-checkin {--with-output : Display the results in a table in your console in addition to sending the email}';
/**
* The console command description.
@@ -42,19 +44,46 @@ class SendExpectedCheckinAlerts extends Command
public function handle()
{
$settings = Setting::getSettings();
$interval = $settings->audit_warning_days ?? 0;
$interval = $settings->due_checkin_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
$count = 0;
if (! $this->option('with-output')) {
$this->info('Run this command with the --with-output option to see the full list in the console.');
}
$assets = Asset::whereNull('deleted_at')->DueOrOverdueForCheckin($settings)->orderBy('assets.expected_checkin', 'desc')->get();
$this->info($assets->count().' assets must be checked in on or before '.$interval_date.' is deadline');
$this->info($assets->count().' assets must be checked on or before '.Helper::getFormattedDateObject($interval_date, 'date', false));
foreach ($assets as $asset) {
if ($asset->assignedTo && (isset($asset->assignedTo->email)) && ($asset->assignedTo->email!='') && $asset->checkedOutToUser()) {
$this->info('Sending User ExpectedCheckinNotification to: '.$asset->assignedTo->email);
if ($asset->assignedTo && (isset($asset->assignedTo->email)) && ($asset->assignedTo->email != '') && $asset->checkedOutToUser()) {
$asset->assignedTo->notify((new ExpectedCheckinNotification($asset)));
$count++;
}
}
if ($this->option('with-output')) {
if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
$this->table(
[
trans('general.id'),
trans('admin/hardware/form.tag'),
trans('admin/hardware/form.model'),
trans('general.model_no'),
trans('general.purchase_date'),
trans('admin/hardware/form.expected_checkin'),
],
$assets->map(fn ($assets) => [
trans('general.id') => $assets->id,
trans('admin/hardware/form.tag') => $assets->asset_tag,
trans('admin/hardware/form.model') => $assets->model->name,
trans('general.model_no') => $assets->model->model_number,
trans('general.purchase_date') => $assets->purchase_date_formatted,
trans('admin/hardware/form.eol_date') => $assets->expected_checkin_formattedDate ? $assets->expected_checkin_formattedDate.' ('.$assets->expected_checkin_diff_for_humans.')' : '',
])
);
}
}
@@ -63,10 +92,11 @@ class SendExpectedCheckinAlerts extends Command
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item) {
return new AlertRecipient($item);
});
$this->info('Sending Admin ExpectedCheckinNotification to: '.$settings->alert_email);
\Notification::send($recipients, new ExpectedCheckinAdminNotification($assets));
Notification::send($recipients, new ExpectedCheckinAdminNotification($assets));
}
$this->info('Sent checkin reminders to to '.$count.' users.');
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Console\Commands;
use App\Helpers\Helper;
use App\Mail\ExpiringAssetsMail;
use App\Mail\ExpiringLicenseMail;
use App\Models\Asset;
@@ -14,11 +13,11 @@ use Illuminate\Support\Facades\Mail;
class SendExpirationAlerts extends Command
{
/**
* The console command name.
* The name and signature of the console command.
*
* @var string
*/
protected $name = 'snipeit:expiring-alerts';
protected $signature = 'snipeit:expiring-alerts {--expired-licenses}';
/**
* The console command description.
@@ -49,12 +48,14 @@ class SendExpirationAlerts extends Command
// 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))
->map(fn ($item) => trim($item)) // Trim each email
->filter(fn ($item) => ! empty($item))
->all();
// Expiring Assets
$assets = Asset::getExpiringWarrantyOrEol($alert_interval);
$assets->load(['assignedTo', 'supplier']);
if ($assets->count() > 0) {
Mail::to($recipients)->send(new ExpiringAssetsMail($assets, $alert_interval));
@@ -70,23 +71,22 @@ class SendExpirationAlerts extends Command
trans('admin/hardware/form.eol_date'),
trans('admin/hardware/form.warranty_expires'),
],
$assets->map(fn($item) =>
[
trans('general.id') => $item->id,
$assets->map(fn ($item) => [
trans('general.id') => $item->id,
trans('admin/hardware/form.tag') => $item->asset_tag,
trans('admin/hardware/form.model') => $item->model->name,
trans('general.model_no') => $item->model->model_number,
trans('general.purchase_date') => $item->purchase_date_formatted,
trans('admin/hardware/form.eol_rate') => $item->model->eol,
trans('admin/hardware/form.eol_date') => $item->eol_date ? $item->eol_formatted_date .' ('.$item->eol_diff_for_humans.')' : '',
trans('admin/hardware/form.warranty_expires') => $item->warranty_expires ? $item->warranty_expires_formatted_date .' ('.$item->warranty_expires_diff_for_humans.')' : '',
])
);
trans('admin/hardware/form.eol_rate') => $item->model->eol,
trans('admin/hardware/form.eol_date') => $item->eol_date ? $item->eol_formatted_date.' ('.$item->eol_diff_for_humans.')' : '',
trans('admin/hardware/form.warranty_expires') => $item->warranty_expires ? $item->warranty_expires_formatted_date.' ('.$item->warranty_expires_diff_for_humans.')' : '',
])
);
}
// Expiring licenses
$licenses = License::query()->ExpiringLicenses($alert_interval)
->with('manufacturer','category')
$licenses = License::query()->ExpiringLicenses($alert_interval, $this->option('expired-licenses'))
->with('manufacturer', 'category')
->orderBy('expiration_date', 'ASC')
->orderBy('termination_date', 'ASC')
->get();
@@ -102,14 +102,14 @@ class SendExpirationAlerts extends Command
trans('mail.expires'),
trans('admin/licenses/form.termination_date'),
trans('mail.terminates')],
$licenses->map(fn($item) => [
$licenses->map(fn ($item) => [
trans('general.id') => $item->id,
trans('general.name') => $item->name,
trans('general.purchase_date') => $item->purchase_date_formatted,
trans('admin/licenses/form.expiration') => $item->expires_formatted_date,
trans('mail.expires') => $item->expires_formatted_date ? $item->expires_diff_for_humans : '',
trans('admin/licenses/form.termination_date') => $item->terminates_formatted_date,
trans('mail.terminates') => $item->terminates_diff_for_humans
trans('mail.terminates') => $item->terminates_diff_for_humans,
])
);
}
@@ -118,12 +118,10 @@ class SendExpirationAlerts extends Command
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $alert_interval]));
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $alert_interval]));
} else {
if ($settings->alert_email == '') {
$this->error('Could not send email. No alert email configured in settings');
} elseif (1 != $settings->alerts_enabled) {
} elseif ($settings->alerts_enabled != 1) {
$this->info('Alerts are disabled in the settings. No mail will be sent');
}
}

View File

@@ -52,12 +52,14 @@ class SendInventoryAlerts extends Command
return new AlertRecipient($item);
});
\Notification::send($recipients, new InventoryAlert($items, $settings->alert_threshold));
Notification::send($recipients, new InventoryAlert($items, $settings->alert_threshold));
} else {
$this->info('No low inventory items found. No mail sent.');
}
} else {
if ($settings->alert_email == '') {
$this->error('Could not send email. No alert email configured in settings');
} elseif (1 != $settings->alerts_enabled) {
} elseif ($settings->alerts_enabled != 1) {
$this->info('Alerts are disabled in the settings. No mail will be sent');
}
}

View File

@@ -49,12 +49,11 @@ class SendUpcomingAuditReport extends Command
$assets_query = Asset::whereNull('deleted_at')->dueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'asc')->with('supplier');
$asset_count = $assets_query->count();
$this->info(number_format($asset_count) . ' assets must be audited on or before ' . $interval_date);
if (!$this->option('with-output')) {
$this->info(number_format($asset_count).' assets must be audited on or before '.$interval_date);
if (! $this->option('with-output')) {
$this->info('Run this command with the --with-output option to see the full list in the console.');
}
if ($asset_count > 0) {
$assets_for_email = $assets_query->limit(30)->get();
@@ -63,22 +62,19 @@ class SendUpcomingAuditReport extends Command
if ($settings->alert_email != '') {
$recipients = collect(explode(',', $settings->alert_email))
->map(fn($item) => trim($item))
->filter(fn($item) => !empty($item))
->map(fn ($item) => trim($item))
->filter(fn ($item) => ! empty($item))
->all();
Mail::to($recipients)->send(new SendUpcomingAuditMail($assets_for_email, $settings->audit_warning_days, $asset_count));
$this->info('Audit notification sent to: ' . $settings->alert_email);
$this->info('Audit notification sent to: '.$settings->alert_email);
} else {
$this->info('There is no admin alert email set so no email will be sent.');
}
if ($this->option('with-output')) {
// Get the full list if the user wants output in the console
$assets_for_output = $assets_query->limit(null)->get();
@@ -93,7 +89,7 @@ class SendUpcomingAuditReport extends Command
trans('mail.assigned_to'),
],
$assets_for_output->map(fn($item) => [
$assets_for_output->map(fn ($item) => [
trans('general.id') => $item->id,
trans('general.name') => $item->display_name,
trans('general.last_audit') => $item->last_audit_formatted_date,
@@ -106,10 +102,8 @@ class SendUpcomingAuditReport extends Command
}
} else {
$this->info('There are no assets due for audit in the next ' . $interval . ' days.');
$this->info('There are no assets due for audit in the next '.$interval.' days.');
}
}
}

View File

@@ -63,16 +63,14 @@ class SyncAssetCounters extends Command
}
} else {
$this->info('No assets to sync');
}
});
} else {
$this->info('No assets to sync');
}
});
$bar->finish();
$time_elapsed_secs = microtime(true) - $start;
$this->info("\nSync of ".$assets_count.' assets executed in '.$time_elapsed_secs.' seconds');
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\Location;
use App\Models\User;
use Illuminate\Console\Command;
class SyncAssetLocations extends Command
@@ -57,7 +59,7 @@ class SyncAssetLocations extends Command
$bar->advance();
}
$assigned_user_assets = Asset::where('assigned_type', \App\Models\User::class)->whereNotNull('assigned_to')->whereNull('deleted_at')->get();
$assigned_user_assets = Asset::where('assigned_type', User::class)->whereNotNull('assigned_to')->whereNull('deleted_at')->get();
$output['info'][] = 'There are '.$assigned_user_assets->count().' assets checked out to users.';
foreach ($assigned_user_assets as $assigned_user_asset) {
if (($assigned_user_asset->assignedTo) && ($assigned_user_asset->assignedTo->userLoc)) {
@@ -73,7 +75,7 @@ class SyncAssetLocations extends Command
$bar->advance();
}
$assigned_location_assets = Asset::where('assigned_type', \App\Models\Location::class)
$assigned_location_assets = Asset::where('assigned_type', Location::class)
->whereNotNull('assigned_to')->whereNull('deleted_at')->get();
$output['info'][] = 'There are '.$assigned_location_assets->count().' assets checked out to locations.';
@@ -90,13 +92,13 @@ class SyncAssetLocations extends Command
}
// Assigned to assets
$assigned_asset_assets = Asset::where('assigned_type', \App\Models\Asset::class)
$assigned_asset_assets = Asset::where('assigned_type', Asset::class)
->whereNotNull('assigned_to')->whereNull('deleted_at')->get();
$output['info'][] = 'Asset-assigned assets: '.$assigned_asset_assets->count();
foreach ($assigned_asset_assets as $assigned_asset_asset) {
// Check to make sure there aren't any invalid relationships
// Check to make sure there aren't any invalid relationships
if ($assigned_asset_asset->assetLoc()) {
$assigned_asset_asset->location_id = $assigned_asset_asset->assetLoc()->id;
$output['info'][] = 'Setting Asset Assigned asset '.$assigned_asset_asset->assetLoc()->id.' ('.$assigned_asset_asset->asset_tag.') location to: '.$assigned_asset_asset->assetLoc()->id;

View File

@@ -37,13 +37,13 @@ class SystemBackup extends Command
*/
public function handle()
{
ini_set('max_execution_time', env('BACKUP_TIME_LIMIT', 600)); //600 seconds = 10 minutes
ini_set('max_execution_time', env('BACKUP_TIME_LIMIT', 600)); // 600 seconds = 10 minutes
if ($this->option('filename')) {
$filename = $this->option('filename');
// Make sure the filename ends in .zip
if (!ends_with($filename, '.zip')) {
if (! ends_with($filename, '.zip')) {
$filename = $filename.'.zip';
}

View File

@@ -47,5 +47,4 @@ class TestLocationsFMCS extends Command
$this->table($header, $mismatched);
}
}

View File

@@ -17,7 +17,6 @@ class ToggleCustomfieldEncryption extends Command
protected $signature = 'snipeit:customfield-encryption
{fieldname : the db_column_name of the field}';
/**
* The console command description.
*
@@ -61,15 +60,15 @@ class ToggleCustomfieldEncryption extends Command
$field->field_encrypted = 1;
$field->save();
// This field is already encrypted. Do nothing.
// This field is already encrypted. Do nothing.
} else {
$this->error('The custom field ' . $field->db_column.' is already encrypted. No action was taken.');
$this->error('The custom field '.$field->db_column.' is already encrypted. No action was taken.');
}
});
// No matching column name found
// No matching column name found
} else {
$this->error('No matching results for unencrypted custom fields with db_column name: ' . $fieldname.'. Please check the fieldname.');
$this->error('No matching results for unencrypted custom fields with db_column name: '.$fieldname.'. Please check the fieldname.');
}
}

View File

@@ -87,7 +87,7 @@ class Version extends Command
$app_version = 'v'."$maj.".($min + 1).".$patch";
} elseif ($use_type == 'patch') {
$app_version = 'v'."$maj.$min.".($patch + 1);
// If nothing is passed, leave the version as it is, just increment the build
// If nothing is passed, leave the version as it is, just increment the build
} else {
$app_version = 'v'."$maj.$min.".$patch;
}

View File

@@ -2,9 +2,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;
@@ -14,12 +11,11 @@ class Kernel extends ConsoleKernel
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
if(Setting::getSettings()?->alerts_enabled === 1) {
if (Setting::getSettings()?->alerts_enabled === 1) {
$schedule->command('snipeit:inventory-alerts')->daily();
$schedule->command('snipeit:expiring-alerts')->daily();
$schedule->command('snipeit:expected-checkin')->daily();

34
app/Enums/ActionType.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
namespace App\Enums;
enum ActionType: string
{
// General
case Create = 'create';
case Update = 'update';
case Delete = 'delete';
case Restore = 'restore';
// Assets/Accessories/Components/Licenses/Consumables
case Checkout = 'checkout';
case CheckinFrom = 'checkin from';
case Requested = 'requested';
case RequestCanceled = 'request canceled';
case Accepted = 'accepted';
case Declined = 'declined';
case Audit = 'audit';
case NoteAdded = 'note added';
// Users
case TwoFactorReset = '2FA reset';
case Merged = 'merged';
// Licenses
case DeleteSeats = 'delete seats';
case AddSeats = 'add seats';
// File Uploads
case Uploaded = 'uploaded';
case UploadDeleted = 'upload deleted';
}

View File

@@ -3,7 +3,6 @@
namespace App\Events;
use App\Models\CheckoutAcceptance;
use App\Models\Contracts\Acceptable;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

View File

@@ -3,7 +3,6 @@
namespace App\Events;
use App\Models\CheckoutAcceptance;
use App\Models\Contracts\Acceptable;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

View File

@@ -11,10 +11,15 @@ class CheckoutableCheckedIn
use Dispatchable, SerializesModels;
public $checkoutable;
public $checkedOutTo;
public $checkedInBy;
public $note;
public $action_date; // Date setted in the hardware.checkin view at the checkin_at input, for the action log
public $originalValues;
/**

View File

@@ -11,22 +11,29 @@ class CheckoutableCheckedOut
use Dispatchable, SerializesModels;
public $checkoutable;
public $checkedOutTo;
public $checkedOutBy;
public $note;
public $originalValues;
public int $quantity;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = [])
public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = [], $quantity = 1)
{
$this->checkoutable = $checkoutable;
$this->checkedOutTo = $checkedOutTo;
$this->checkedOutBy = $checkedOutBy;
$this->note = $note;
$this->originalValues = $originalValues;
$this->quantity = $quantity;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
class CheckoutablesCheckedOutInBulk
{
use Dispatchable, SerializesModels;
public function __construct(
public Collection $assets,
public Model $target,
public User $admin,
public string $checkout_at,
public string $expected_checkin,
public string $note,
) {}
}

View File

@@ -2,9 +2,9 @@
namespace App\Events;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
class UserMerged
{
@@ -17,8 +17,8 @@ class UserMerged
*/
public function __construct(User $from_user, User $to_user, ?User $admin)
{
$this->merged_from = $from_user;
$this->merged_to = $to_user;
$this->admin = $admin;
$this->merged_from = $from_user;
$this->merged_to = $to_user;
$this->admin = $admin;
}
}

View File

@@ -4,6 +4,4 @@ namespace App\Exceptions;
use Exception;
class AssetNotRequestable extends Exception
{
}
class AssetNotRequestable extends Exception {}

View File

@@ -2,16 +2,25 @@
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use App\Helpers\Helper;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\AuthenticationException;
use ArieTimmerman\Laravel\SCIMServer\Exceptions\SCIMException;
use Illuminate\Support\Facades\Log;
use Throwable;
use JsonException;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Session\TokenMismatchException;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use Intervention\Image\Exception\NotSupportedException;
use JsonException;
use League\OAuth2\Server\Exception\OAuthServerException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class Handler extends ExceptionHandler
{
@@ -21,16 +30,16 @@ class Handler extends ExceptionHandler
* @var array
*/
protected $dontReport = [
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Auth\Access\AuthorizationException::class,
\Symfony\Component\HttpKernel\Exception\HttpException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Illuminate\Session\TokenMismatchException::class,
\Illuminate\Validation\ValidationException::class,
\Intervention\Image\Exception\NotSupportedException::class,
\League\OAuth2\Server\Exception\OAuthServerException::class,
AuthenticationException::class,
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
TokenMismatchException::class,
ValidationException::class,
NotSupportedException::class,
OAuthServerException::class,
JsonException::class,
SCIMException::class, //these generally don't need to be reported
SCIMException::class, // these generally don't need to be reported
InvalidFormatException::class,
];
@@ -39,7 +48,6 @@ class Handler extends ExceptionHandler
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Throwable $exception
* @return void
*/
public function report(Throwable $exception)
@@ -48,23 +56,23 @@ class Handler extends ExceptionHandler
if (class_exists(Log::class)) {
Log::error($exception);
}
return parent::report($exception);
}
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
*
* @param Request $request
* @param \Exception $e
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse|\Illuminate\Http\Response
* @return JsonResponse|RedirectResponse|Response
*/
public function render($request, Throwable $e)
{
// CSRF token mismatch error
if ($e instanceof \Illuminate\Session\TokenMismatchException) {
if ($e instanceof TokenMismatchException) {
return redirect()->back()->with('error', trans('general.token_expired'));
}
@@ -78,9 +86,10 @@ class Handler extends ExceptionHandler
if ($e instanceof SCIMException) {
try {
$e->report(); // logs as 'debug', so shouldn't get too noisy
} catch(\Exception $reportException) {
//do nothing
} catch (\Exception $reportException) {
// do nothing
}
return $e->render($request); // ALL SCIMExceptions have the 'render()' method
}
@@ -98,9 +107,10 @@ class Handler extends ExceptionHandler
}
// Handle API requests that fail because the model doesn't exist
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
if ($e instanceof ModelNotFoundException) {
$className = last(explode('\\', $e->getModel()));
return response()->json(Helper::formatStandardApiResponse('error', null, $className . ' not found'), 200);
return response()->json(Helper::formatStandardApiResponse('error', null, $className.' not found'), 200);
}
// Handle API requests that fail because of an HTTP status code and return a useful error message
@@ -111,8 +121,8 @@ class Handler extends ExceptionHandler
// 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 '405':
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode.' endpoint not found'), 404);
case '405':
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
default:
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode);
@@ -124,19 +134,20 @@ class Handler extends ExceptionHandler
// 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);
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) {
if ($e instanceof ModelNotFoundException) {
$ids = method_exists($e, 'getIds') ? $e->getIds() : [];
if (in_array('bulkedit', $ids, true)) {
$error_array = session()->get('bulk_asset_errors');
$error_array = session()->get('bulk_asset_errors');
return redirect()
->route('hardware.index')
->withErrors($error_array, 'bulk_asset_errors')
@@ -144,7 +155,7 @@ class Handler extends ExceptionHandler
}
// 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()))))));
$model_name = trim(strtolower(implode(' ', preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel()))))));
$route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
// Sigh.
@@ -162,6 +173,8 @@ class Handler extends ExceptionHandler
$route = 'licenses.index';
} elseif (($route === 'customfieldsets.index') || ($route === 'customfields.index')) {
$route = 'fields.index';
} elseif ($route == 'actionlogs.index') {
$route = 'home';
}
return redirect()
@@ -169,24 +182,22 @@ class Handler extends ExceptionHandler
->withError(trans('general.generic_model_not_found', ['model' => $model_name]));
}
if ($this->isHttpException($e) && (isset($statusCode)) && ($statusCode == '404' )) {
if ($this->isHttpException($e) && (isset($statusCode)) && ($statusCode == '404')) {
return response()->view('layouts/basic', [
'content' => view('errors/404')
],$statusCode);
'content' => view('errors/404'),
], $statusCode);
}
return parent::render($request, $e);
}
/**
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Auth\AuthenticationException $exception
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
*/
* @param Request $request
* @return JsonResponse|RedirectResponse
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
@@ -201,8 +212,7 @@ class Handler extends ExceptionHandler
return response()->json(Helper::formatStandardApiResponse('error', null, $exception->errors()), 200);
}
/**
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array

View File

@@ -2,8 +2,6 @@
namespace App\Exceptions;
use Exception;
class ItemStillHasAccessories extends ItemStillHasChildren
{
//

View File

@@ -2,8 +2,6 @@
namespace App\Exceptions;
use Exception;
class ItemStillHasAssetModels extends ItemStillHasChildren
{
//

View File

@@ -2,8 +2,4 @@
namespace App\Exceptions;
use Exception;
class ItemStillHasAssets extends ItemStillHasChildren
{
}
class ItemStillHasAssets extends ItemStillHasChildren {}

View File

@@ -6,9 +6,9 @@ use Exception;
class ItemStillHasChildren extends Exception
{
//public function __construct($message, $code = 0, Exception $previous = null, $parent, $children)
//{
// public function __construct($message, $code = 0, Exception $previous = null, $parent, $children)
// {
// trans()
//
//}
// }
}

View File

@@ -2,8 +2,6 @@
namespace App\Exceptions;
use Exception;
class ItemStillHasComponents extends ItemStillHasChildren
{
//

View File

@@ -2,8 +2,6 @@
namespace App\Exceptions;
use Exception;
class ItemStillHasConsumables extends ItemStillHasChildren
{
//

View File

@@ -2,8 +2,6 @@
namespace App\Exceptions;
use Exception;
class ItemStillHasLicenses extends ItemStillHasChildren
{
//

View File

@@ -2,8 +2,6 @@
namespace App\Exceptions;
use Exception;
class ItemStillHasMaintenances extends ItemStillHasChildren
{
//

View File

@@ -4,7 +4,4 @@ namespace App\Exceptions;
use Exception;
class UserDoestExistException extends Exception
{
}
class UserDoestExistException extends Exception {}

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,15 @@ namespace App\Helpers;
class IconHelper
{
public static function icon($type) {
public static function icon($type)
{
switch ($type) {
case 'checkout':
return 'fa-solid fa-rotate-left';
case 'checkin':
return 'fa-solid fa-rotate-right';
case 'edit':
case 'update':
return 'fas fa-pencil-alt';
case 'clone':
return 'far fa-clone';
@@ -36,6 +37,8 @@ class IconHelper
return 'fa-solid fa-user';
case 'users':
return 'fas fa-users';
case 'supplier':
return 'fa-solid fa-store';
case 'restore':
return 'fa-solid fa-trash-arrow-up';
case 'external-link':
@@ -46,6 +49,8 @@ class IconHelper
return 'fa-regular fa-envelope';
case 'phone':
return 'fa-solid fa-phone';
case 'fax':
return 'fa-solid fa-fax';
case 'mobile':
return 'fas fa-mobile-screen-button';
case 'long-arrow-right':
@@ -53,7 +58,7 @@ class IconHelper
case 'download':
return 'fas fa-download';
case 'checkmark':
return 'fas fa-check icon-white';
return 'fas fa-check';
case 'x':
return 'fas fa-times';
case 'logout':
@@ -85,8 +90,11 @@ class IconHelper
case 'licenses':
case 'license':
return 'far fa-save';
case 'requests':
case 'requestable':
return 'fas fa-laptop';
case 'request':
case 'requested':
return 'fa-solid fa-bell-concierge';
case 'reports':
return 'fas fa-chart-bar';
case 'heart':
@@ -112,7 +120,7 @@ class IconHelper
case 'dashboard':
return 'fas fa-tachometer-alt';
case 'info-circle':
return 'fas fa-info-circle';
return 'fas fa-info-circle';
case 'caret-right':
return 'fa fa-caret-right';
case 'caret-up':
@@ -129,9 +137,12 @@ class IconHelper
return 'fa-regular fa-clipboard';
case 'paperclip':
return 'fas fa-paperclip';
case 'contact-card':
return 'fa-regular fa-id-card';
case 'files':
return 'fa-regular fa-file';
case 'more-info':
case 'support':
return 'far fa-life-ring';
case 'calendar':
return 'fas fa-calendar';
@@ -188,11 +199,11 @@ class IconHelper
return 'fas fa-crosshairs';
case 'oauth':
return 'fas fa-user-secret';
case 'employee_num' :
case 'employee_num':
return 'fa-regular fa-id-card';
case 'department' :
case 'department':
return 'fa-solid fa-building-user';
case 'home' :
case 'home':
return 'fa-solid fa-house';
case 'note':
case 'notes':
@@ -201,6 +212,54 @@ class IconHelper
return 'fa-solid fa-lightbulb';
case 'highlight':
return 'fa-solid fa-highlighter';
case 'manager':
return 'fa-solid fa-building-user';
case 'company':
return 'fa-regular fa-building';
case 'parent':
return 'fa-solid fa-building-flag';
case 'number':
return 'fa-solid fa-hashtag';
case 'depreciation':
return 'fa-solid fa-arrows-down-to-line';
case 'depreciation-calendar':
case 'expiration':
case 'terminates':
return 'fa-regular fa-calendar-xmark';
case 'manufacturer':
return 'fa-solid fa-industry';
case 'fieldset':
return 'fa-regular fa-rectangle-list';
case 'deleted-date':
return 'fa-solid fa-calendar-xmark';
case 'eol':
return 'fa-regular fa-calendar-days';
case 'category':
return 'fa-solid fa-icons';
case 'cost':
return 'fa-solid fa-money-bills';
case 'available':
return 'fa-solid fa-box';
case 'checkedout':
return 'fa-solid fa-box-open';
case 'purchase_order':
return 'fa-solid fa-file-invoice-dollar';
case 'order':
return 'fa-solid fa-file-invoice';
case 'checkout-all':
return 'fa-solid fa-arrows-down-to-people';
case 'square-right':
return 'fa-regular fa-square-caret-right';
case 'square-left':
return 'fa-regular fa-square-caret-left';
case 'square':
return 'fa-solid fa-square';
case 'models':
case 'model':
return 'fa-solid fa-boxes-stacked';
case 'min-qty':
return 'fa-solid fa-chart-pie';
}
}
}

View File

@@ -2,32 +2,33 @@
namespace App\Helpers;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Response;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
class StorageHelper
{
public static function downloader($filename, $disk = 'default') : BinaryFileResponse | RedirectResponse | StreamedResponse
public static function downloader($filename, $disk = 'default'): BinaryFileResponse|RedirectResponse|StreamedResponse
{
if ($disk == 'default') {
$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) {
public static function getMediaType($file_with_path)
{
// Get the file extension and determine the media type
if (Storage::exists($file_with_path)) {
@@ -64,6 +65,7 @@ class StorageHelper
return $extension; // Default for unknown types
}
}
return null;
}
@@ -72,8 +74,9 @@ class StorageHelper
* to determine that they are safe to display inline.
*
* @author <A. Gianotto> [<snipe@snipe.net]>
*
* @since v7.0.14
* @param $file_with_path
*
* @return bool
*/
public static function allowSafeInline($file_with_path)
@@ -96,11 +99,11 @@ class StorageHelper
'webp',
];
// The file exists and is allowed to be displayed inline
if (Storage::exists($file_with_path) && (in_array(pathinfo($file_with_path, PATHINFO_EXTENSION), $allowed_inline))) {
return true;
}
return false;
}
@@ -117,7 +120,6 @@ class StorageHelper
}
/**
* Decide whether to show the file inline or download it.
*/
@@ -140,7 +142,7 @@ class StorageHelper
// Everything else seems okay, but the file doesn't exist on the server.
if (Storage::missing($file)) {
throw new FileNotFoundException();
throw new FileNotFoundException;
}
return Storage::download($file, $filename, $headers);

View File

@@ -7,11 +7,11 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Accessory;
use App\Models\Company;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
/** This controller handles all actions related to Accessories for
* the Snipe-IT Asset Management application.
@@ -25,12 +25,14 @@ class AccessoriesController extends Controller
* the content for the accessories listing, which is generated in getDatatable.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @see AccessoriesController::getDatatable() method that generates the JSON response
* @since [v1.0]
*/
public function index() : View
public function index(): View
{
$this->authorize('index', Accessory::class);
return view('accessories.index');
}
@@ -39,43 +41,42 @@ class AccessoriesController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function create() : View
public function create(): View
{
$this->authorize('create', Accessory::class);
$category_type = 'accessory';
return view('accessories/edit')->with('category_type', $category_type)
->with('item', new Accessory);
->with('item', new Accessory);
}
/**
* Validate and save new Accessory from form post
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param ImageUploadRequest $request
*/
public function store(ImageUploadRequest $request) : RedirectResponse
public function store(ImageUploadRequest $request): RedirectResponse
{
$this->authorize(Accessory::class);
// create a new model instance
$accessory = new Accessory();
$accessory = new Accessory;
// Update the accessory data
$accessory->name = request('name');
$accessory->category_id = request('category_id');
$accessory->location_id = request('location_id');
$accessory->min_amt = request('min_amt');
$accessory->company_id = Company::getIdForCurrentUser(request('company_id'));
$accessory->order_number = request('order_number');
$accessory->manufacturer_id = request('manufacturer_id');
$accessory->model_number = request('model_number');
$accessory->purchase_date = request('purchase_date');
$accessory->purchase_cost = request('purchase_cost');
$accessory->qty = request('qty');
$accessory->created_by = auth()->id();
$accessory->supplier_id = request('supplier_id');
$accessory->notes = request('notes');
$accessory->name = request('name');
$accessory->category_id = request('category_id');
$accessory->location_id = request('location_id');
$accessory->min_amt = request('min_amt');
$accessory->company_id = Company::getIdForCurrentUser(request('company_id'));
$accessory->order_number = request('order_number');
$accessory->manufacturer_id = request('manufacturer_id');
$accessory->model_number = request('model_number');
$accessory->purchase_date = request('purchase_date');
$accessory->purchase_cost = request('purchase_cost');
$accessory->qty = request('qty');
$accessory->created_by = auth()->id();
$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'));
@@ -90,10 +91,10 @@ class AccessoriesController extends Controller
$accessory = $request->handleImages($accessory);
}
if($request->get('redirect_option') === 'back'){
if ($request->input('redirect_option') === 'back') {
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
session()->put(['redirect_option' => $request->input('redirect_option')]);
}
// Was the accessory created?
@@ -110,11 +111,14 @@ class AccessoriesController extends Controller
* Return view for the Accessory update form, prepopulated with existing data
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $accessoryId
*
* @param int $accessoryId
*/
public function edit(Accessory $accessory) : View | RedirectResponse
public function edit(Accessory $accessory): View|RedirectResponse
{
$this->authorize('update', Accessory::class);
$this->authorize('update', $accessory);
session()->put('url.intended', url()->previous());
return view('accessories.edit')->with('item', $accessory)->with('category_type', 'accessory');
}
@@ -122,13 +126,15 @@ class AccessoriesController extends Controller
* Returns a view that presents a form to clone an accessory.
*
* @author [J. Vinsmoke]
* @param int $accessoryId
*
* @param int $accessoryId
*
* @since [v6.0]
*/
public function getClone(Accessory $accessory) : View | RedirectResponse
public function getClone(Accessory $accessory): View|RedirectResponse
{
$this->authorize('create', Accessory::class);
$this->authorize('create', $accessory);
$cloned = clone $accessory;
$accessory_to_clone = $accessory;
$cloned->id = null;
@@ -137,24 +143,24 @@ class AccessoriesController extends Controller
return view('accessories/edit')
->with('cloned_model', $accessory_to_clone)
->with('item', $cloned);
}
/**
* Save edited Accessory from form post
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param ImageUploadRequest $request
* @param int $accessoryId
*
* @param int $accessoryId
*/
public function update(ImageUploadRequest $request, Accessory $accessory) : RedirectResponse
public function update(ImageUploadRequest $request, Accessory $accessory): RedirectResponse
{
$this->authorize('update', $accessory);
if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessory->id)) {
$this->authorize($accessory);
$validator = Validator::make($request->all(), [
"qty" => "required|numeric|min:$accessory->checkouts_count"
'qty' => "required|numeric|min:$accessory->checkouts_count",
]);
if ($validator->fails()) {
@@ -163,8 +169,6 @@ class AccessoriesController extends Controller
->withInput();
}
// Update the accessory data
$accessory->name = request('name');
$accessory->location_id = request('location_id');
@@ -182,7 +186,11 @@ class AccessoriesController extends Controller
$accessory = $request->handleImages($accessory);
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($request->input('redirect_option') === 'back') {
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->input('redirect_option')]);
}
if ($accessory->save()) {
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
@@ -199,51 +207,48 @@ class AccessoriesController extends Controller
* Delete the given accessory.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $accessoryId
*
* @param int $accessoryId
*/
public function destroy($accessoryId) : RedirectResponse
public function destroy(Accessory $accessory): RedirectResponse
{
if (is_null($accessory = Accessory::withCount('checkouts as checkouts_count')->find($accessoryId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
}
$this->authorize('delete', $accessory);
$accessory->loadCount('checkouts as checkouts_count');
$this->authorize($accessory);
if ($accessory->checkouts_count > 0) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/general.delete_disabled'));
}
if ($accessory->image) {
try {
Storage::disk('public')->delete('accessories'.'/'.$accessory->image);
} catch (\Exception $e) {
Log::debug($e);
if ($accessory->isDeletable()) {
if ($accessory->image) {
try {
Storage::disk('public')->delete('accessories'.'/'.$accessory->image);
} catch (\Exception $e) {
Log::debug($e);
}
}
$accessory->delete();
return redirect()->route('accessories.index')->with('success', trans('admin/accessories/message.delete.success'));
}
$accessory->delete();
return redirect()->route('accessories.index')->with('success', trans('admin/accessories/message.delete.success'));
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/general.delete_disabled'));
}
/**
* Returns a view that invokes the ajax table which contains
* the content for the accessory detail view, which is generated in getDataView.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $accessoryID
*
* @param int $accessoryID
*
* @see AccessoriesController::getDataView() method that generates the JSON response
* @since [v1.0]
*/
public function show(Accessory $accessory) : View | RedirectResponse
public function show(Accessory $accessory): View|RedirectResponse
{
$accessory->loadCount('checkouts as checkouts_count');
$accessory->load(['adminuser' => fn($query) => $query->withTrashed()]);
$this->authorize('view', $accessory);
$accessory->loadCount('checkouts as checkouts_count');
$accessory->load(['adminuser' => fn ($query) => $query->withTrashed()]);
return view('accessories.view', compact('accessory'));
}
}

View File

@@ -7,10 +7,10 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
class AccessoryCheckinController extends Controller
{
@@ -18,25 +18,26 @@ class AccessoryCheckinController extends Controller
* Check the accessory back into inventory
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param Request $request
* @param int $accessoryUserId
* @param string $backto
*
* @param Request $request
* @param int $accessoryUserId
* @param string $backto
*/
public function create($accessoryUserId = null, $backto = null) : View | RedirectResponse
public function create($accessoryUserId = null, $backto = null): View|RedirectResponse
{
if (is_null($accessory_user = DB::table('accessories_checkout')->find($accessoryUserId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
}
$accessory = Accessory::find($accessory_user->accessory_id);
$this->authorize('checkin', $accessory);
//based on what the accessory is checked out to the target redirect option will be displayed accordingly.
// 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);
@@ -46,17 +47,20 @@ class AccessoryCheckinController extends Controller
* Check in the item so that it can be checked out again to someone else
*
* @uses Accessory::checkin_email() to determine if an email can and should be sent
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param null $accessoryCheckoutId
* @param string $backto
*
* @param null $accessoryCheckoutId
* @param string $backto
*/
public function store(Request $request, $accessoryCheckoutId = null, $backto = null) : RedirectResponse
public function store(Request $request, $accessoryCheckoutId = null, $backto = null): RedirectResponse
{
if (is_null($accessory_checkout = AccessoryCheckout::find($accessoryCheckoutId))) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.does_not_exist'));
}
$accessory = Accessory::find($accessory_checkout->accessory_id);
$this->authorize('checkin', $accessory);
session()->put('checkedInFrom', $accessory_checkout->assigned_to);
session()->put('checkout_to_type', match ($accessory_checkout->assigned_type) {
@@ -65,7 +69,6 @@ class AccessoryCheckinController extends Controller
'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')) {
@@ -76,11 +79,12 @@ class AccessoryCheckinController extends Controller
if ($accessory_checkout->delete()) {
event(new CheckoutableCheckedIn($accessory, $accessory_checkout->assignedTo, auth()->user(), $request->input('note'), $checkin_at));
session()->put(['redirect_option' => $request->get('redirect_option')]);
session()->put(['redirect_option' => $request->input('redirect_option')]);
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')
->with('success', trans('admin/accessories/message.checkin.success'));
}
// Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkin.error'));
}

View File

@@ -11,47 +11,39 @@ use App\Models\Accessory;
use App\Models\AccessoryCheckout;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
class AccessoryCheckoutController extends Controller
{
use CheckInOutRequest;
/**
* Return the form to checkout an Accessory to a user.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $id
*
* @param int $id
*/
public function create($id) : View | RedirectResponse
public function create(Accessory $accessory): View|RedirectResponse
{
if ($accessory = Accessory::withCount('checkouts as checkouts_count')->find($id)) {
$this->authorize('checkout', $accessory);
$this->authorize('checkout', $accessory);
if ($accessory->category) {
// Make sure there is at least one available to checkout
if ($accessory->numRemaining() <= 0){
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkout.unavailable'));
}
// Return the checkout view
return view('accessories/checkout', compact('accessory'));
if ($accessory->category) {
// Make sure there is at least one available to checkout
if ($accessory->numRemaining() <= 0) {
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkout.unavailable'));
}
// Invalid category
return redirect()->route('accessories.edit', ['accessory' => $accessory->id])
->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.accessory')]));
// Return the checkout view
return view('accessories/checkout', compact('accessory'));
}
// Not found
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
// Invalid category
return redirect()->route('accessories.edit', ['accessory' => $accessory->id])
->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.accessory')]));
}
@@ -62,19 +54,19 @@ class AccessoryCheckoutController extends Controller
* trigger a Slack message and send an email.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param Request $request
* @param Accessory $accessory
*
* @param Request $request
*/
public function store(AccessoryCheckoutRequest $request, Accessory $accessory) : RedirectResponse
public function store(AccessoryCheckoutRequest $request, Accessory $accessory): RedirectResponse
{
$this->authorize('checkout', $accessory);
$target = $this->determineCheckoutTarget();
session()->put(['checkout_to_type' => $target]);
$accessory->checkout_qty = $request->input('checkout_qty', 1);
for ($i = 0; $i < $accessory->checkout_qty; $i++) {
$accessory_checkout = new AccessoryCheckout([
@@ -89,13 +81,19 @@ class AccessoryCheckoutController extends Controller
$accessory_checkout->save();
}
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
event(new CheckoutableCheckedOut(
$accessory,
$target,
auth()->user(),
$request->input('note'),
[],
$accessory->checkout_qty,
));
$request->request->add(['checkout_to_type' => request('checkout_to_type')]);
$request->request->add(['assigned_to' => $target->id]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
session()->put(['redirect_option' => $request->input('redirect_option'), 'checkout_to_type' => $request->input('checkout_to_type')]);
// Redirect to the new accessory page
return Helper::getRedirectOption($request, $accessory->id, 'Accessories')

View File

@@ -4,36 +4,34 @@ namespace App\Http\Controllers\Account;
use App\Events\CheckoutAccepted;
use App\Events\CheckoutDeclined;
use App\Events\ItemAccepted;
use App\Events\ItemDeclined;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Mail\CheckoutAcceptanceResponseMail;
use App\Models\CheckoutAcceptance;
use App\Models\Company;
use App\Models\Contracts\Acceptable;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\AcceptanceAssetAcceptedNotification;
use App\Notifications\AcceptanceAssetAcceptedToUserNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification;
use App\Notifications\AcceptanceItemAcceptedNotification;
use App\Notifications\AcceptanceItemAcceptedToUserNotification;
use App\Notifications\AcceptanceItemDeclinedNotification;
use Exception;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use \Illuminate\Contracts\View\View;
use \Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Log;
use App\Helpers\Helper;
class AcceptanceController extends Controller
{
/**
* Show a listing of pending checkout acceptances for the current user
*/
public function index() : View
public function index(): View
{
$acceptances = CheckoutAcceptance::forUser(auth()->user())->pending()->get();
return view('account/accept.index', compact('acceptances'));
}
@@ -42,11 +40,10 @@ class AcceptanceController extends Controller
*
* @param int $id
*/
public function create($id) : View | RedirectResponse
public function create($id): View|RedirectResponse
{
$acceptance = CheckoutAcceptance::find($id);
if (is_null($acceptance)) {
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
}
@@ -69,21 +66,19 @@ class AcceptanceController extends Controller
/**
* Stores the accept/decline of the checkout acceptance
*
* @param Request $request
* @param int $id
*/
public function store(Request $request, $id) : RedirectResponse
public function store(Request $request, $id): RedirectResponse
{
$acceptance = CheckoutAcceptance::find($id);
$assigned_user = User::find($acceptance->assigned_to_id);
$settings = Setting::getSettings();
$sig_filename='';
if (is_null($acceptance)) {
if (! $acceptance = CheckoutAcceptance::find($id)) {
return redirect()->route('account.accept')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$assigned_user = User::find($acceptance->assigned_to_id);
$settings = Setting::getSettings();
$sig_filename = '';
if (! $acceptance->isPending()) {
return redirect()->route('account.accept')->with('error', trans('admin/users/message.error.asset_already_accepted'));
}
@@ -121,11 +116,11 @@ class AcceptanceController extends Controller
// The item was accepted, check for a signature
if ($request->filled('signature_output')) {
$sig_filename = 'siglog-' . Str::uuid() . '-' . date('Y-m-d-his') . '.png';
$sig_filename = 'siglog-'.Str::uuid().'-'.date('Y-m-d-his').'.png';
$data_uri = $request->input('signature_output');
$encoded_image = explode(',', $data_uri);
$decoded_image = base64_decode($encoded_image[1]);
Storage::put('private_uploads/signatures/' . $sig_filename, (string)$decoded_image);
Storage::put('private_uploads/signatures/'.$sig_filename, (string) $decoded_image);
// No image data is present, kick them back.
// This mostly only applies to users on super-duper crapola browsers *cough* IE *cough*
@@ -134,18 +129,17 @@ class AcceptanceController extends Controller
}
}
// Convert PDF logo to base64 for TCPDF
// This is needed for TCPDF to properly embed the image if it's a png and the cache isn't writable
$encoded_logo = null;
if ($settings->acceptance_pdf_logo) {
$encoded_logo = base64_encode(file_get_contents(public_path() . '/uploads/' . $settings->acceptance_pdf_logo));
if (($settings->acceptance_pdf_logo) && (Storage::disk('public')->exists($settings->acceptance_pdf_logo))) {
$encoded_logo = base64_encode(file_get_contents(public_path().'/uploads/'.$settings->acceptance_pdf_logo));
}
// Get the data array ready for the notifications and PDF generation
$data = [
'item_tag' => $item->asset_tag,
'item_name' => $item->name, // this handles licenses seats, which don't have a 'name' field
'item_name' => $item->display_name, // this handles licenses seats, which don't have a 'name' field
'item_model' => $item->model?->name,
'item_serial' => $item->serial,
'item_status' => $item->assetstatus?->name,
@@ -158,7 +152,7 @@ class AcceptanceController extends Controller
'email' => $assigned_user->email,
'employee_num' => $assigned_user->employee_num,
'site_name' => $settings->site_name,
'company_name' => $item->company?->name?? $settings->site_name,
'company_name' => $item->company?->name ?? $settings->site_name,
'signature' => (($sig_filename && array_key_exists('1', $encoded_image))) ? $encoded_image[1] : null,
'logo' => ($encoded_logo) ?? null,
'date_settings' => $settings->date_display_format,
@@ -167,56 +161,54 @@ class AcceptanceController extends Controller
if ($request->input('asset_acceptance') == 'accepted') {
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
// Generate the PDF content
$pdf_content = $acceptance->generateAcceptancePdf($data, $acceptance);
Storage::put('private_uploads/eula-pdfs/' .$pdf_filename, $pdf_content);
Storage::put('private_uploads/eula-pdfs/'.$pdf_filename, $pdf_content);
// Log the acceptance
$acceptance->accept($sig_filename, $item->getEula(), $pdf_filename, $request->input('note'));
// Send the PDF to the signing user
if (($request->input('send_copy') == '1') && ($assigned_user->email !='')) {
if (($request->input('send_copy') == '1') && ($assigned_user->email != '')) {
// Add the attachment for the signing user into the $data array
$data['file'] = $pdf_filename;
try {
$assigned_user->notify((new AcceptanceAssetAcceptedToUserNotification($data))->locale($assigned_user->locale));
} catch (\Exception $e) {
$assigned_user->notify((new AcceptanceItemAcceptedToUserNotification($data))->locale($assigned_user->locale));
} catch (Exception $e) {
Log::warning($e);
}
}
try {
$acceptance->notify((new AcceptanceAssetAcceptedNotification($data))->locale(Setting::getSettings()->locale));
} catch (\Exception $e) {
$acceptance->notify((new AcceptanceItemAcceptedNotification($data))->locale(Setting::getSettings()->locale));
} catch (Exception $e) {
Log::warning($e);
}
event(new CheckoutAccepted($acceptance));
$return_msg = trans('admin/users/message.accepted');
// Item was declined
// Item was declined
} else {
for ($i = 0; $i < ($acceptance->qty ?? 1); $i++) {
$acceptance->decline($sig_filename, $request->input('note'));
}
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
$acceptance->notify(new AcceptanceItemDeclinedNotification($data));
Log::debug('New event acceptance.');
event(new CheckoutDeclined($acceptance));
$return_msg = trans('admin/users/message.declined');
}
// Send an email notification if one is requested
if ($acceptance->alert_on_response_id) {
try {
$recipient = User::find($acceptance->alert_on_response_id);
if ($recipient) {
if ($recipient?->email) {
Log::debug('Attempting to send email acceptance.');
Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail(
$acceptance,
@@ -230,10 +222,8 @@ class AcceptanceController extends Controller
Log::warning($e);
}
}
return redirect()->to('account/accept')->with('success', $return_msg);
}
}

View File

@@ -4,17 +4,18 @@ namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use \Illuminate\Http\Response;
class ActionlogController extends Controller
{
public function displaySig($filename) : RedirectResponse | Response | bool
public function displaySig($filename): RedirectResponse|Response|bool
{
// PHP doesn't let you handle file not found errors well with
// PHP doesn't let you handle file not found errors well with
// file_get_contents, so we set the error reporting for just this class
error_reporting(0);
@@ -23,15 +24,17 @@ class ActionlogController extends Controller
case 's3':
$file = 'private_uploads/signatures/'.$filename;
return redirect()->away(Storage::disk($disk)->temporaryUrl($file, now()->addMinutes(5)));
default:
$this->authorize('view', \App\Models\Asset::class);
$this->authorize('view', Asset::class);
$file = config('app.private_uploads').'/signatures/'.$filename;
$filetype = Helper::checkUploadIsImage($file);
$contents = file_get_contents($file, false, stream_context_create(['http' => ['ignore_errors' => true]]));
if ($contents === false) {
Log::warning('File '.$file.' not found');
return false;
} else {
return response()->make($contents)->header('Content-Type', $filetype);
@@ -39,7 +42,7 @@ class ActionlogController extends Controller
}
}
public function getStoredEula($filename) : Response | BinaryFileResponse | RedirectResponse
public function getStoredEula($filename): Response|BinaryFileResponse|RedirectResponse
{
if ($actionlog = Actionlog::where('filename', $filename)->with('user')->with('target')->firstOrFail()) {
@@ -47,18 +50,17 @@ class ActionlogController extends Controller
$this->authorize('view', $actionlog->target);
$this->authorize('view', $actionlog->user);
if (config('filesystems.default') == 's3_private') {
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/' . $filename, now()->addMinutes(5)));
return redirect()->away(Storage::disk('s3_private')->temporaryUrl('private_uploads/eula-pdfs/'.$filename, now()->addMinutes(5)));
}
if (Storage::exists('private_uploads/eula-pdfs/' . $filename)) {
if (Storage::exists('private_uploads/eula-pdfs/'.$filename)) {
if (request()->input('inline') == 'true') {
return response()->file(config('app.private_uploads') . '/eula-pdfs/' . $filename);
return response()->file(config('app.private_uploads').'/eula-pdfs/'.$filename);
}
return response()->download(config('app.private_uploads') . '/eula-pdfs/' . $filename);
return response()->download(config('app.private_uploads').'/eula-pdfs/'.$filename);
}
return redirect()->back()->with('error', trans('general.file_does_not_exist'));

View File

@@ -7,19 +7,17 @@ use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Controllers\Controller;
use App\Http\Requests\AccessoryCheckoutRequest;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\StoreAccessoryRequest;
use App\Http\Transformers\AccessoriesTransformer;
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;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use App\Models\AccessoryCheckout;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class AccessoriesController extends Controller
{
@@ -29,8 +27,10 @@ class AccessoriesController extends Controller
* Display a listing of the resource.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @return \Illuminate\Http\Response
*
* @return Response
*/
public function index(Request $request)
{
@@ -38,10 +38,9 @@ class AccessoriesController extends Controller
$this->authorize('view', Accessory::class);
}
// This array is what determines which fields should be allowed to be sorted on ON the table itself, no relations
// Relations will be handled in query scopes a little further down.
$allowed_columns =
$allowed_columns =
[
'id',
'name',
@@ -53,6 +52,7 @@ class AccessoriesController extends Controller
'company_id',
'notes',
'checkouts_count',
'order_number',
'qty',
// These are *relationships* so we wouldn't normally include them in this array,
// since they would normally create a `column not found` error,
@@ -65,7 +65,6 @@ class AccessoriesController extends Controller
'manufacturer',
];
$accessories = Accessory::select('accessories.*')
->with('category', 'company', 'manufacturer', 'checkouts', 'location', 'supplier', 'adminuser')
->withCount('checkouts as checkouts_count');
@@ -86,11 +85,14 @@ class AccessoriesController extends Controller
$accessories->TextSearch($request->input('search'));
}
if ($request->filled('company_id')) {
$accessories->where('accessories.company_id', '=', $request->input('company_id'));
}
if ($request->filled('order_number')) {
$accessories->where('accessories.order_number', '=', $request->input('order_number'));
}
if ($request->filled('category_id')) {
$accessories->where('category_id', '=', $request->input('category_id'));
}
@@ -104,11 +106,11 @@ class AccessoriesController extends Controller
}
if ($request->filled('location_id')) {
$accessories->where('location_id','=',$request->input('location_id'));
$accessories->where('location_id', '=', $request->input('location_id'));
}
if ($request->filled('notes')) {
$accessories->where('notes','=',$request->input('notes'));
$accessories->where('notes', '=', $request->input('notes'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
@@ -116,7 +118,7 @@ class AccessoriesController extends Controller
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort_override = $request->input('sort');
$sort_override = $request->input('sort');
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'created_at';
switch ($sort_override) {
@@ -131,7 +133,7 @@ class AccessoriesController extends Controller
break;
case 'manufacturer':
$accessories = $accessories->OrderManufacturer($order);
break;
break;
case 'supplier':
$accessories = $accessories->OrderSupplier($order);
break;
@@ -142,20 +144,21 @@ class AccessoriesController extends Controller
$accessories = $accessories->orderBy($column_sort, $order);
break;
}
$total = $accessories->count();
$accessories = $accessories->skip($offset)->take($limit)->get();
return (new AccessoriesTransformer)->transformAccessories($accessories, $total);
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\ImageUploadRequest $request
* @return \Illuminate\Http\JsonResponse
* @param ImageUploadRequest $request
* @return JsonResponse
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*/
public function store(StoreAccessoryRequest $request)
@@ -177,7 +180,9 @@ class AccessoriesController extends Controller
*
* @param int $id
* @return array
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*/
public function show($id)
@@ -188,13 +193,14 @@ class AccessoriesController extends Controller
return (new AccessoriesTransformer)->transformAccessory($accessory);
}
/**
* Display the specified resource.
*
* @param int $id
* @return array
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*/
public function accessory_detail($id)
@@ -205,14 +211,15 @@ class AccessoriesController extends Controller
return (new AccessoriesTransformer)->transformAccessory($accessory);
}
/**
* Get the list of checkouts for a specific accessory
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*
* @param int $id
* @return | array
* @return | array
*/
public function checkedout(Request $request, $id)
{
@@ -236,15 +243,15 @@ class AccessoriesController extends Controller
return (new AccessoriesTransformer)->transformCheckedoutAccessory($accessory_checkouts, $total);
}
/**
* Update the specified resource in storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @param \App\Http\Requests\ImageUploadRequest $request
*
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function update(ImageUploadRequest $request, $id)
{
@@ -264,9 +271,11 @@ class AccessoriesController extends Controller
* Remove the specified resource from storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function destroy($id)
{
@@ -283,7 +292,6 @@ class AccessoriesController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/accessories/message.delete.success')));
}
/**
* Save the Accessory checkout information.
*
@@ -291,7 +299,8 @@ class AccessoriesController extends Controller
* trigger a Slack message and send an email.
*
* @param int $accessoryId
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function checkout(AccessoryCheckoutRequest $request, Accessory $accessory)
@@ -310,7 +319,6 @@ class AccessoriesController extends Controller
'note' => $request->input('note'),
]);
$accessory_checkout->created_by = auth()->id();
$accessory_checkout->save();
@@ -325,7 +333,14 @@ class AccessoriesController extends Controller
}
// Set this value to be able to pass the qty through to the event
event(new CheckoutableCheckedOut($accessory, $target, auth()->user(), $request->input('note')));
event(new CheckoutableCheckedOut(
$accessory,
$target,
auth()->user(),
$request->input('note'),
[],
$accessory->checkout_qty,
));
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkout.success')));
@@ -334,12 +349,14 @@ class AccessoriesController extends Controller
/**
* Check in the item so that it can be checked out again to someone else
*
* @param Request $request
* @param int $accessoryUserId
* @param string $backto
* @param int $accessoryUserId
* @param string $backto
* @return JsonResponse
*
* @uses Accessory::checkin_email() to determine if an email can and should be sent
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @internal param int $accessoryId
*/
public function checkin(Request $request, $accessoryUserId = null)
@@ -366,20 +383,18 @@ class AccessoriesController extends Controller
'pivot' => $accessory_checkout->id,
];
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkin.success')));
return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('admin/accessories/message.checkin.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/accessories/message.checkin.error')));
}
/**
* Gets a paginated collection for the select2 menus
*
* @see \App\Http\Transformers\SelectlistTransformer
*
*/
* Gets a paginated collection for the select2 menus
*
* @see SelectlistTransformer
*/
public function selectlist(Request $request)
{
@@ -389,12 +404,11 @@ class AccessoriesController extends Controller
]);
if ($request->filled('search')) {
$accessories = $accessories->where('accessories.name', 'LIKE', '%'.$request->get('search').'%');
$accessories = $accessories->where('accessories.name', 'LIKE', '%'.$request->input('search').'%');
}
$accessories = $accessories->orderBy('name', 'ASC')->paginate(50);
return (new SelectlistTransformer)->transformSelectlist($accessories);
}
}

View File

@@ -4,22 +4,26 @@ namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\StoreAssetModelRequest;
use App\Http\Transformers\AssetModelsTransformer;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Asset;
use App\Models\AssetModel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
/**
* This class controls all actions related to asset models for
* the Snipe-IT Asset Management application.
*
* @version v4.0
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
class AssetModelsController extends Controller
@@ -28,9 +32,10 @@ class AssetModelsController extends Controller
* Display a listing of the resource.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*/
public function index(Request $request) : JsonResponse | array
public function index(Request $request): JsonResponse|array
{
$this->authorize('view', AssetModel::class);
$allowed_columns =
@@ -79,8 +84,8 @@ class AssetModelsController extends Controller
'models.fieldset_id',
'models.deleted_at',
'models.updated_at',
'models.require_serial'
])
'models.require_serial',
])
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues', 'adminuser')
->withCount('assets as assets_count')
->withCount('availableAssets as remaining')
@@ -104,8 +109,7 @@ class AssetModelsController extends Controller
$assetmodels->TextSearch($request->input('search'));
}
if ($request->input('status')=='deleted') {
if ($request->input('status') == 'deleted') {
$assetmodels->onlyTrashed();
}
@@ -121,7 +125,7 @@ class AssetModelsController extends Controller
$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'));
@@ -170,15 +174,14 @@ class AssetModelsController extends Controller
return (new AssetModelsTransformer)->transformAssetModels($assetmodels, $total);
}
/**
* Store a newly created resource in storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @param \App\Http\Requests\StoreAssetModelRequest $request
*/
public function store(StoreAssetModelRequest $request) : JsonResponse
public function store(StoreAssetModelRequest $request): JsonResponse
{
$this->authorize('create', AssetModel::class);
$assetmodel = new AssetModel;
@@ -188,8 +191,8 @@ class AssetModelsController extends Controller
if ($assetmodel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
}
@@ -197,10 +200,12 @@ class AssetModelsController extends Controller
* Display the specified resource.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*
* @param int $id
*/
public function show($id) : array
public function show($id): array
{
$this->authorize('view', AssetModel::class);
$assetmodel = AssetModel::withCount('assets as assets_count')->findOrFail($id);
@@ -212,10 +217,12 @@ class AssetModelsController extends Controller
* Display the specified resource's assets
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*
* @param int $id
*/
public function assets($id) : array
public function assets($id): array
{
$this->authorize('view', AssetModel::class);
$assets = Asset::where('model_id', '=', $id)->get();
@@ -223,17 +230,18 @@ class AssetModelsController extends Controller
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
}
/**
* Update the specified resource in storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @param \App\Http\Requests\ImageUploadRequest $request
*
* @param ImageUploadRequest $request
* @param int $id
* @return \Illuminate\Http\Response
* @return Response
*/
public function update(StoreAssetModelRequest $request, $id) : JsonResponse
public function update(StoreAssetModelRequest $request, $id): JsonResponse
{
$this->authorize('update', AssetModel::class);
$assetmodel = AssetModel::findOrFail($id);
@@ -249,10 +257,9 @@ class AssetModelsController extends Controller
* it, but I'll be damned if I can think of one. - snipe
*/
if ($request->filled('custom_fieldset_id')) {
$assetmodel->fieldset_id = $request->get('custom_fieldset_id');
$assetmodel->fieldset_id = $request->input('custom_fieldset_id');
}
if ($assetmodel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.update.success')));
}
@@ -264,10 +271,12 @@ class AssetModelsController extends Controller
* Remove the specified resource from storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*
* @param int $id
*/
public function destroy($id) : JsonResponse
public function destroy($id): JsonResponse
{
$this->authorize('delete', AssetModel::class);
$assetmodel = AssetModel::findOrFail($id);
@@ -294,10 +303,11 @@ class AssetModelsController extends Controller
* Gets a paginated collection for the select2 menus
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0.16]
* @see \App\Http\Transformers\SelectlistTransformer
* @see SelectlistTransformer
*/
public function selectlist(Request $request) : array
public function selectlist(Request $request): array
{
$this->authorize('view.selectlists');
@@ -310,7 +320,7 @@ class AssetModelsController extends Controller
'models.category_id',
])->with('manufacturer', 'category');
$settings = \App\Models\Setting::getSettings();
$settings = Setting::getSettings();
if ($request->filled('search')) {
$assetmodels = $assetmodels->SearchByManufacturerOrCat($request->input('search'));

View File

@@ -3,46 +3,49 @@
namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedIn;
use App\Http\Requests\StoreAssetRequest;
use App\Http\Requests\UpdateAssetRequest;
use App\Http\Traits\MigratesLegacyAssetLocations;
use App\Http\Transformers\ComponentsTransformer;
use App\Models\AccessoryCheckout;
use App\Models\CheckoutAcceptance;
use App\Models\LicenseSeat;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\AssetCheckoutRequest;
use App\Http\Requests\FilterRequest;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\StoreAssetRequest;
use App\Http\Requests\UpdateAssetRequest;
use App\Http\Traits\MigratesLegacyAssetLocations;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\ComponentsTransformer;
use App\Http\Transformers\LicensesTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\AccessoryCheckout;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CheckoutAcceptance;
use App\Models\Company;
use App\Models\CustomField;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use App\Observers\AssetObserver;
use App\View\Label;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
use App\View\Label;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
/**
* This class controls all actions related to assets for
* the Snipe-IT Asset Management application.
*
* @version v1.0
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
class AssetsController extends Controller
@@ -53,13 +56,14 @@ class AssetsController extends Controller
* Returns JSON listing of all assets
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
*
* @param int $assetId
*
* @since [v4.0]
*/
public function index(Request $request, $action = null, $upcoming_status = null) : JsonResponse | array
public function index(FilterRequest $request, $action = null, $upcoming_status = null): JsonResponse|array
{
// This handles the legacy audit endpoints :(
if ($action == 'audit') {
$action = 'audits';
@@ -67,17 +71,17 @@ class AssetsController extends Controller
$filter_non_deprecable_assets = false;
/**
* This looks MAD janky (and it is), but the AssetsController@index does a LOT of heavy lifting throughout the
* app. This bit here just makes sure that someone without permission to view assets doesn't
* end up with priv escalations because they asked for a different endpoint.
*
* Since we never gave the specification for which transformer to use before, it should default
* gracefully to just use the AssetTransformer by default, which shouldn't break anything.
*
* It was either this mess, or repeating ALL of the searching and sorting and filtering code,
* This looks MAD janky (and it is), but the AssetsController@index does a LOT of heavy lifting throughout the
* app. This bit here just makes sure that someone without permission to view assets doesn't
* end up with priv escalations because they asked for a different endpoint.
*
* Since we never gave the specification for which transformer to use before, it should default
* gracefully to just use the AssetTransformer by default, which shouldn't break anything.
*
* It was either this mess, or repeating ALL of the searching and sorting and filtering code,
* which would have been far worse of a mess. *sad face* - snipe (Sept 1, 2021)
*/
if (Route::currentRouteName()=='api.depreciation-report.index') {
if (Route::currentRouteName() == 'api.depreciation-report.index') {
$filter_non_deprecable_assets = true;
$transformer = 'App\Http\Transformers\DepreciationReportTransformer';
$this->authorize('reports.view');
@@ -86,7 +90,6 @@ class AssetsController extends Controller
$this->authorize('index', Asset::class);
}
$settings = Setting::getSettings();
$allowed_columns = [
@@ -134,7 +137,7 @@ class AssetsController extends Controller
];
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
$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();
@@ -152,6 +155,15 @@ class AssetsController extends Controller
}
$assets = Asset::select('assets.*')
// ->addSelect([
// 'first_checkout_at' => Actionlog::query()
// ->select('created_at')
// ->whereColumn('item_id', 'assets.id')
// ->where('item_type', Asset::class)
// ->where('action_type', 'checkout')
// ->orderBy('created_at')
// ->limit(1),
// ])
->with(
'model',
'location',
@@ -168,14 +180,11 @@ class AssetsController extends Controller
'supplier'
); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
if ($filter_non_deprecable_assets) {
$non_deprecable_models = AssetModel::select('id')->whereNotNull('depreciation_id')->get();
$assets->InModelList($non_deprecable_models->toArray());
}
// These are used by the API to query against specific ID numbers.
// They are also used by the individual searches on detail pages like
// locations, etc.
@@ -193,12 +202,11 @@ class AssetsController extends Controller
$assets->TextSearch($request->input('search'));
}
/**
* Handle due and overdue audits and checkin dates
*/
switch ($action) {
// Audit (singular) is left over from earlier legacy APIs
// Audit (singular) is left over from earlier legacy APIs
case 'audits':
switch ($upcoming_status) {
case 'due':
@@ -232,7 +240,6 @@ class AssetsController extends Controller
* End handling due and overdue audits and checkin dates
*/
// This is used by the sidenav, mostly
// We switched from using query scopes here because of a Laravel bug
@@ -306,7 +313,6 @@ class AssetsController extends Controller
}
}
// Leave these under the TextSearch scope, else the fuzziness will override the specific ID (status ID, etc) requested
if ($request->filled('status_id')) {
$assets->where('assets.status_id', '=', $request->input('status_id'));
@@ -323,7 +329,7 @@ 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'))) {
@@ -376,7 +382,7 @@ class AssetsController extends Controller
}
if ($request->filled('order_number')) {
$assets->where('assets.order_number', '=', strval($request->get('order_number')));
$assets->where('assets.order_number', '=', strval($request->input('order_number')));
}
// This is kinda gross, but we need to do this because the Bootstrap Tables
@@ -442,7 +448,7 @@ class AssetsController extends Controller
// This may not work for all databases, but it works for MySQL
if ($numeric_sort) {
$assets->orderByRaw(DB::getTablePrefix() . 'assets.' . $sort_override . ' * 1 ' . $order);
$assets->orderByRaw(DB::getTablePrefix().'assets.'.$sort_override.' * 1 '.$order);
} else {
$assets->orderBy($sort_override, $order);
}
@@ -452,7 +458,6 @@ class AssetsController extends Controller
break;
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $assets->count()) ? $assets->count() : app('api_offset_value');
$limit = app('api_limit_value');
@@ -460,7 +465,6 @@ class AssetsController extends Controller
$total = $assets->count();
$assets = $assets->skip($offset)->take($limit)->get();
/**
* Include additional associated relationships
*/
@@ -473,15 +477,16 @@ class AssetsController extends Controller
return (new $transformer)->transformAssets($assets, $total, $request);
}
/**
* Returns JSON with information about an asset (by tag) for detail view.
*
* @param string $tag
* @param string $tag
*
* @since [v4.2.1]
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function showByTag(Request $request, $tag): JsonResponse | array
public function showByTag(Request $request, $tag): JsonResponse|array
{
$this->authorize('index', Asset::class);
$assets = Asset::where('asset_tag', $tag)->with('assetstatus')->with('assignedTo');
@@ -513,11 +518,14 @@ class AssetsController extends Controller
* Returns JSON with information about an asset (by serial) for detail view.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param string $serial
*
* @param string $serial
*
* @since [v4.2.1]
* @return \Illuminate\Http\JsonResponse
*
* @return JsonResponse
*/
public function showBySerial(Request $request, $serial): JsonResponse | array
public function showBySerial(Request $request, $serial): JsonResponse|array
{
$this->authorize('index', Asset::class);
$assets = Asset::where('serial', $serial)->with([
@@ -556,11 +564,14 @@ class AssetsController extends Controller
* Returns JSON with information about an asset for detail view.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
*
* @param int $assetId
*
* @since [v4.0]
* @return \Illuminate\Http\JsonResponse
*
* @return JsonResponse
*/
public function show(Request $request, $id): JsonResponse | array
public function show(Request $request, $id): JsonResponse|array
{
if ($asset = Asset::with('assetstatus')
->with('assignedTo')->withTrashed()
@@ -570,6 +581,7 @@ class AssetsController extends Controller
return (new AssetsTransformer)->transformAsset($asset, $request->input('components'));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
}
@@ -580,16 +592,16 @@ class AssetsController extends Controller
$asset = Asset::where('id', $id)->withTrashed()->firstorfail();
$licenses = $asset->licenses()->get();
return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count());
return (new LicensesTransformer)->transformLicenses($licenses, $licenses->count());
}
/**
* Gets a paginated collection for the select2 menus
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0.16]
* @see \App\Http\Transformers\SelectlistTransformer
* @see SelectlistTransformer
*/
public function selectlist(Request $request): array
{
@@ -605,7 +617,7 @@ class AssetsController extends Controller
])->with('model', 'assetstatus', 'assignedTo')
->NotArchived();
if ((Setting::getSettings()->full_multiple_companies_support=='1') && ($request->filled('companyId'))) {
if ((Setting::getSettings()->full_multiple_companies_support == '1') && ($request->filled('companyId'))) {
$assets->where('assets.company_id', $request->input('companyId'));
}
@@ -624,16 +636,14 @@ class AssetsController extends Controller
// they may not have a ->name value but we want to display something anyway
foreach ($assets as $asset) {
$asset->use_text = $asset->present()->fullName;
if (($asset->checkedOutToUser()) && ($asset->assigned)) {
$asset->use_text .= ' → ' . $asset->assigned->display_name;
$asset->use_text .= ' → '.$asset->assigned->display_name;
}
if ($asset->assetstatus->getStatuslabelType() == 'pending') {
$asset->use_text .= '(' . $asset->assetstatus->getStatuslabelType() . ')';
$asset->use_text .= '('.$asset->assetstatus->getStatuslabelType().')';
}
$asset->use_image = ($asset->getImageUrl()) ? $asset->getImageUrl() : null;
@@ -642,21 +652,22 @@ class AssetsController extends Controller
return (new SelectlistTransformer)->transformSelectlist($assets);
}
/**
* Accepts a POST request to create a new asset
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param \App\Http\Requests\ImageUploadRequest $request
*
* @param ImageUploadRequest $request
*
* @since [v4.0]
*/
public function store(StoreAssetRequest $request): JsonResponse
{
$asset = new Asset();
$asset->model()->associate(AssetModel::find((int) $request->get('model_id')));
$asset = new Asset;
$asset->model()->associate(AssetModel::find((int) $request->input('model_id')));
$asset->fill($request->validated());
$asset->created_by = auth()->id();
$asset->created_by = auth()->id();
/**
* this is here just legacy reasons. Api\AssetController
@@ -681,9 +692,9 @@ class AssetsController extends Controller
// If input value is null, use custom field's default value
if ($field_val == null) {
Log::debug('Field value for ' . $field->db_column . ' is null');
$field_val = $field->defaultValue($request->get('model_id'));
Log::debug('Use the default fieldset value of ' . $field->defaultValue($request->get('model_id')));
Log::debug('Field value for '.$field->db_column.' is null');
$field_val = $field->defaultValue($request->input('model_id'));
Log::debug('Use the default fieldset value of '.$field->defaultValue($request->input('model_id')));
}
// if the field is set to encrypted, make sure we encrypt the value
@@ -694,7 +705,7 @@ class AssetsController extends Controller
// If input value is null, use custom field's default value
if (($field_val == null) && ($request->has('model_id') != '')) {
$field_val = Crypt::encrypt($field->defaultValue($request->get('model_id')));
$field_val = Crypt::encrypt($field->defaultValue($request->input('model_id')));
} else {
$field_val = Crypt::encrypt($request->input($field->db_column));
}
@@ -706,21 +717,20 @@ class AssetsController extends Controller
}
}
$asset->{$field->db_column} = $field_val;
}
}
if ($asset->save()) {
if ($request->get('assigned_user')) {
if ($request->input('assigned_user')) {
$target = User::find(request('assigned_user'));
} elseif ($request->get('assigned_asset')) {
} elseif ($request->input('assigned_asset')) {
$target = Asset::find(request('assigned_asset'));
} elseif ($request->get('assigned_location')) {
} elseif ($request->input('assigned_location')) {
$target = Location::find(request('assigned_location'));
}
if (isset($target)) {
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e($request->get('name')));
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e($request->input('name')));
}
if ($asset->image) {
@@ -737,11 +747,11 @@ class AssetsController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
}
/**
* Accepts a POST request to update an asset
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*/
public function update(UpdateAssetRequest $request, Asset $asset): JsonResponse
@@ -754,7 +764,7 @@ class AssetsController extends Controller
if ($request->has('company_id')) {
$asset->company_id = Company::getIdForCurrentUser($request->validated()['company_id']);
}
if ($request->has('rtd_location_id') && !$request->has('location_id')) {
if ($request->has('rtd_location_id') && ! $request->has('location_id')) {
$asset->location_id = $request->validated()['rtd_location_id'];
}
if ($request->input('last_audit_date')) {
@@ -789,6 +799,7 @@ class AssetsController extends Controller
$field_val = Crypt::encrypt($field_val);
} else {
$problems_updating_encrypted_custom_fields = true;
continue;
}
}
@@ -797,19 +808,22 @@ class AssetsController extends Controller
}
}
if ($asset->save()) {
if (($request->filled('assigned_user')) && ($target = User::find($request->get('assigned_user')))) {
if (($request->filled('assigned_user')) && ($target = User::find($request->input('assigned_user')))) {
$location = $target->location_id;
} elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->get('assigned_asset')))) {
} elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->input('assigned_asset')))) {
$location = $target->location_id;
Asset::where('assigned_type', \App\Models\Asset::class)->where('assigned_to', $asset->id)
Asset::where('assigned_type', Asset::class)->where('assigned_to', $asset->id)
->update(['location_id' => $target->location_id]);
} elseif (($request->filled('assigned_location')) && ($target = Location::find($request->get('assigned_location')))) {
} elseif (($request->filled('assigned_location')) && ($target = Location::find($request->input('assigned_location')))) {
$location = $target->id;
}
if (isset($target)) {
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', e($request->get('name')), $location);
// Using `->has` preserves the asset name if the name parameter was not included in request.
$asset_name = request()->has('name') ? request('name') : $asset->name;
$asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', $asset_name, $location);
}
if ($asset->image) {
@@ -823,18 +837,20 @@ class AssetsController extends Controller
} 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);
}
/**
* Delete a given asset (mark as deleted).
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
*
* @param int $assetId
*
* @since [v4.0]
*/
public function destroy($id): JsonResponse
@@ -863,13 +879,13 @@ class AssetsController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
}
/**
* Restore a soft-deleted asset.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
*
* @param int $assetId
*
* @since [v5.1.18]
*/
public function restore(Request $request, $assetId = null): JsonResponse
@@ -897,7 +913,9 @@ class AssetsController extends Controller
* Checkout an asset by its tag.
*
* @author [N. Butler]
* @param string $tag
*
* @param string $tag
*
* @since [v6.0.5]
*/
public function checkoutByTag(AssetCheckoutRequest $request, $tag): JsonResponse
@@ -905,6 +923,7 @@ class AssetsController extends Controller
if ($asset = Asset::where('asset_tag', $tag)->first()) {
return $this->checkout($request, $asset->id);
}
return response()->json(Helper::formatStandardApiResponse('error', null, 'Asset not found'), 200);
}
@@ -912,7 +931,9 @@ class AssetsController extends Controller
* Checkout an asset
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
*
* @param int $assetId
*
* @since [v4.0]
*/
public function checkout(AssetCheckoutRequest $request, $asset_id): JsonResponse
@@ -953,11 +974,11 @@ class AssetsController extends Controller
}
if ($request->filled('status_id')) {
$asset->status_id = $request->get('status_id');
$asset->status_id = $request->input('status_id');
}
if (! isset($target)) {
return response()->json(Helper::formatStandardApiResponse('error', $error_payload, 'Checkout target for asset ' . e($asset->asset_tag) . ' is invalid - ' . $error_payload['target_type'] . ' does not exist.'));
return response()->json(Helper::formatStandardApiResponse('error', $error_payload, 'Checkout target for asset '.e($asset->asset_tag).' is invalid - '.$error_payload['target_type'].' does not exist.'));
}
$checkout_at = request('checkout_at', date('Y-m-d H:i:s'));
@@ -968,8 +989,7 @@ class AssetsController extends Controller
// Set the location ID to the RTD location id if there is one
// Wait, why are we doing this? This overrides the stuff we set further up, which makes no sense.
// TODO: Follow up here. WTF. Commented out for now.
// TODO: Follow up here. WTF. Commented out for now.
// if ((isset($target->rtd_location_id)) && ($asset->rtd_location_id!='')) {
// $asset->location_id = $target->rtd_location_id;
@@ -982,12 +1002,13 @@ class AssetsController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', ['asset' => e($asset->asset_tag)], trans('admin/hardware/message.checkout.error')));
}
/**
* Checkin an asset
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
*
* @param int $assetId
*
* @since [v4.0]
*/
public function checkin(Request $request, $asset_id): JsonResponse
@@ -1000,12 +1021,12 @@ class AssetsController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', [
'asset_tag' => e($asset->asset_tag),
'model' => e($asset->model->name),
'model_number' => e($asset->model->model_number)
'model_number' => e($asset->model->model_number),
], trans('admin/hardware/message.checkin.already_checked_in')));
}
$asset->expected_checkin = null;
//$asset->last_checkout = null;
// $asset->last_checkout = null;
$asset->last_checkin = now();
$asset->assignedTo()->disassociate($asset);
$asset->accepted = null;
@@ -1030,10 +1051,10 @@ class AssetsController extends Controller
$asset->status_id = $request->input('status_id');
}
$checkin_at = $request->filled('checkin_at') ? $request->input('checkin_at') . ' ' . date('H:i:s') : date('Y-m-d H:i:s');
$checkin_at = $request->filled('checkin_at') ? $request->input('checkin_at').' '.date('H:i:s') : date('Y-m-d H:i:s');
$originalValues = $asset->getRawOriginal();
if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) {
if (($request->filled('checkin_at')) && ($request->input('checkin_at') != date('Y-m-d'))) {
$originalValues['action_date'] = $checkin_at;
}
@@ -1061,7 +1082,7 @@ class AssetsController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', [
'asset_tag' => e($asset->asset_tag),
'model' => e($asset->model->name),
'model_number' => e($asset->model->model_number)
'model_number' => e($asset->model->model_number),
], trans('admin/hardware/message.checkin.success')));
}
@@ -1072,12 +1093,13 @@ class AssetsController extends Controller
* Checkin an asset by asset tag
*
* @author [A. Janes] [<ajanes@adagiohealth.org>]
*
* @since [v6.0]
*/
public function checkinByTag(Request $request, $tag = null): JsonResponse
{
$this->authorize('checkin', Asset::class);
if (null == $tag && null !== ($request->input('asset_tag'))) {
if ($tag == null && null !== ($request->input('asset_tag'))) {
$tag = $request->input('asset_tag');
}
$asset = Asset::where('asset_tag', $tag)->first();
@@ -1087,20 +1109,20 @@ class AssetsController extends Controller
}
return response()->json(Helper::formatStandardApiResponse('error', [
'asset' => e($tag)
], 'Asset with tag ' . e($tag) . ' not found'));
'asset' => e($tag),
], 'Asset with tag '.e($tag).' not found'));
}
/**
* Mark an asset as audited
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $id
*
* @param int $id
*
* @since [v4.0]
*/
public function audit(Request $request, Asset $asset): JsonResponse
{
$this->authorize('audit', Asset::class);
@@ -1134,20 +1156,21 @@ class AssetsController extends Controller
$payload = [
'id' => $asset->id,
'asset_tag' => $asset->asset_tag,
'note' => $request->input('note'),
'note' => e($request->input('note')),
'status_label' => e($asset->assetstatus?->display_name),
'status_type' => $asset->assetstatus->getStatuslabelType(),
'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'));
*/
* $model = AssetModel::find($request->input('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->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))) {
@@ -1163,7 +1186,7 @@ class AssetsController extends Controller
$asset->{$field->db_column} = $request->input($field->db_column);
}
}
$payload['custom_fields'][$field->db_column] = $request->input($field->db_column);
$payload['custom_fields'][$field->db_column] = $request->input($field->db_column);
}
}
@@ -1174,10 +1197,9 @@ class AssetsController extends Controller
// 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()));
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
@@ -1191,12 +1213,10 @@ class AssetsController extends Controller
* 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()
* @see AssetObserver::updating()
* @see Asset::save()
*/
$asset->unsetEventDispatcher();
$asset->unsetEventDispatcher();
/**
* Invoke Watson Validating to check the asset itself and check to make sure it saved correctly.
@@ -1204,26 +1224,25 @@ class AssetsController extends Controller
*/
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')));
}
}
// 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', null, trans('admin/hardware/message.does_not_exist')), 200);
}
/**
* Returns JSON listing of all requestable assets
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*/
public function requestable(Request $request): JsonResponse | array
public function requestable(Request $request): JsonResponse|array
{
$this->authorize('viewRequestable', Asset::class);
@@ -1237,7 +1256,7 @@ class AssetsController extends Controller
'expected_checkin',
];
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
$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();
@@ -1257,9 +1276,6 @@ class AssetsController extends Controller
'requests'
);
if ($request->filled('search')) {
$assets->TextSearch($request->input('search'));
}
@@ -1305,8 +1321,7 @@ class AssetsController extends Controller
return (new AssetsTransformer)->transformRequestedAssets($assets, $total);
}
public function assignedAssets(Request $request, Asset $asset) : JsonResponse | array
public function assignedAssets(Request $request, Asset $asset): JsonResponse|array
{
$this->authorize('view', Asset::class);
$this->authorize('view', $asset);
@@ -1323,7 +1338,7 @@ class AssetsController extends Controller
return (new AssetsTransformer)->transformAssets($assets, $total);
}
public function assignedAccessories(Request $request, Asset $asset) : JsonResponse | array
public function assignedAccessories(Request $request, Asset $asset): JsonResponse|array
{
$this->authorize('view', Asset::class);
$this->authorize('view', $asset);
@@ -1337,6 +1352,7 @@ class AssetsController extends Controller
$total = $accessory_checkouts->count();
$accessory_checkouts = $accessory_checkouts->skip($offset)->take($limit)->get();
return (new AssetsTransformer)->transformCheckedoutAccessories($accessory_checkouts, $total);
}
@@ -1348,17 +1364,17 @@ class AssetsController extends Controller
$asset->loadCount('components');
$total = $asset->components_count;
$components = $asset->load(['components' => fn($query) => $query->applyOffsetAndLimit($total)])->components;
$components = $asset->load(['components' => fn ($query) => $query->applyOffsetAndLimit($total)])->components;
return (new ComponentsTransformer)->transformComponents($components, $total);
}
/**
* Generate asset labels by tag
*
*
* @author [Nebelkreis] [https://github.com/NebelKreis]
*
* @param Request $request Contains asset_tags array of asset tags to generate labels for
*
* @param Request $request Contains asset_tags array of asset tags to generate labels for
* @return JsonResponse Returns base64 encoded PDF on success, error message on failure
*/
public function getLabels(Request $request): JsonResponse
@@ -1366,17 +1382,17 @@ class AssetsController extends Controller
try {
$this->authorize('view', Asset::class);
// Validate that asset tags were provided in the request
if (!$request->filled('asset_tags')) {
return response()->json(Helper::formatStandardApiResponse('error', null,
// Validate that asset tags were provided in the request
if (! $request->filled('asset_tags')) {
return response()->json(Helper::formatStandardApiResponse('error', null,
trans('admin/hardware/message.no_assets_selected')), 400);
}
// Convert asset tags from request into collection and fetch matching assets
// Convert asset tags from request into collection and fetch matching assets
$asset_tags = collect($request->input('asset_tags'));
$assets = Asset::whereIn('asset_tag', $asset_tags)->get();
// Return error if no assets were found for the provided tags
// Return error if no assets were found for the provided tags
if ($assets->isEmpty()) {
return response()->json(Helper::formatStandardApiResponse('error', null,
trans('admin/hardware/message.does_not_exist')), 404);
@@ -1387,27 +1403,27 @@ class AssetsController extends Controller
// Check if logo file exists in storage and disable logo if not found
// This prevents errors when trying to include a non-existent logo in the PDF
$settings->label_logo = ($original_logo = $settings->label_logo) && !Storage::disk('public')->exists('/' . $original_logo) ? null : $settings->label_logo;
$settings->label_logo = ($original_logo = $settings->label_logo) && ! Storage::disk('public')->exists('/'.$original_logo) ? null : $settings->label_logo;
$label = new Label;
$label = new Label();
if (!$label) {
if (! $label) {
throw new \Exception('Label object could not be created');
}
// Configure label with assets and settings
// bulkedit=false and count=0 are default values for label generation
$label = $label->with('assets', $assets)
->with('settings', $settings)
->with('bulkedit', false)
->with('count', 0);
->with('settings', $settings)
->with('bulkedit', false)
->with('count', 0);
// Generate PDF using callback function
// The callback captures the PDF content in $pdf_content variable
$pdf_content = '';
$label->render(function($pdf) use (&$pdf_content) {
$label->render(function ($pdf) use (&$pdf_content) {
$pdf_content = $pdf->Output('', 'S');
return $pdf;
});
@@ -1419,21 +1435,21 @@ class AssetsController extends Controller
$encoded_content = base64_encode($pdf_content);
return response()->json(Helper::formatStandardApiResponse('success', [
'pdf' => $encoded_content
'pdf' => $encoded_content,
], trans('admin/hardware/message.labels_generated')));
} catch (\Exception $e) {
return response()->json(Helper::formatStandardApiResponse('error', [
'error_message' => $e->getMessage(),
'error_line' => $e->getLine(),
'error_file' => $e->getFile()
'error_file' => $e->getFile(),
], trans('admin/hardware/message.error_generating_labels')), 500);
}
} catch (\Exception $e) {
return response()->json(Helper::formatStandardApiResponse('error', [
'error_message' => $e->getMessage(),
'error_line' => $e->getLine(),
'error_file' => $e->getFile()
'error_file' => $e->getFile(),
], $e->getMessage()), 500);
}
}

View File

@@ -6,12 +6,13 @@ use App\Actions\Categories\DestroyCategoryAction;
use App\Exceptions\ItemStillHasChildren;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Transformers\CategoriesTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
class CategoriesController extends Controller
@@ -20,10 +21,12 @@ class CategoriesController extends Controller
* Display a listing of the resource.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @return \Illuminate\Http\Response
*
* @return Response
*/
public function index(Request $request) : array
public function index(Request $request): array
{
$this->authorize('view', Category::class);
$allowed_columns = [
@@ -43,6 +46,7 @@ class CategoriesController extends Controller
'created_at',
'updated_at',
'image',
'tag_color',
'notes',
];
@@ -57,12 +61,12 @@ class CategoriesController extends Controller
'require_acceptance',
'checkin_email',
'image',
'tag_color',
'notes',
])
])
->with('adminuser')
->withCount('accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count', 'models as models_count');
$filter = [];
if ($request->filled('filter')) {
@@ -87,7 +91,7 @@ class CategoriesController extends Controller
*
* @see \App\Models\Category::showableAssets()
*/
if ($request->input('archived')=='true') {
if ($request->input('archived') == 'true') {
$categories = $categories->withCount('assets as assets_count');
} else {
$categories = $categories->withCount('showableAssets as assets_count');
@@ -129,7 +133,7 @@ class CategoriesController extends Controller
$offset = ($request->input('offset') > $categories->count()) ? $categories->count() : app('api_offset_value');
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort_override = $request->input('sort');
$sort_override = $request->input('sort');
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets_count';
switch ($sort_override) {
@@ -148,16 +152,16 @@ class CategoriesController extends Controller
}
/**
* Store a newly created resource in storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @param \App\Http\Requests\ImageUploadRequest $request
* @return \Illuminate\Http\Response
*
* @return Response
*/
public function store(ImageUploadRequest $request) : JsonResponse
public function store(ImageUploadRequest $request): JsonResponse
{
$this->authorize('create', Category::class);
$category = new Category;
@@ -168,6 +172,7 @@ class CategoriesController extends Controller
if ($category->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $category, trans('admin/categories/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $category->getErrors()));
}
@@ -176,28 +181,31 @@ class CategoriesController extends Controller
* Display the specified resource.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*
* @param int $id
*/
public function show($id) : array
public function show($id): array
{
$this->authorize('view', Category::class);
$category = Category::withCount('assets as assets_count', 'accessories as accessories_count', 'consumables as consumables_count', 'components as components_count', 'licenses as licenses_count')->findOrFail($id);
return (new CategoriesTransformer)->transformCategory($category);
}
/**
* Update the specified resource in storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @param \App\Http\Requests\ImageUploadRequest $request
*
* @param int $id
* @return \Illuminate\Http\Response
* @return Response
*/
public function update(ImageUploadRequest $request, $id) : JsonResponse
public function update(ImageUploadRequest $request, $id): JsonResponse
{
$this->authorize('update', Category::class);
$category = Category::findOrFail($id);
@@ -205,7 +213,7 @@ class CategoriesController extends Controller
// Don't allow the user to change the category_type once it's been created
if (($request->filled('category_type')) && ($category->category_type != $request->input('category_type'))) {
return response()->json(
Helper::formatStandardApiResponse('error', null, ['category_type' => trans('admin/categories/message.update.cannot_change_category_type')], 422)
Helper::formatStandardApiResponse('error', null, ['category_type' => trans('admin/categories/message.update.cannot_change_category_type')], 422)
);
}
$category->fill($request->all());
@@ -222,9 +230,11 @@ class CategoriesController extends Controller
* Remove the specified resource from storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*
* @param int $id
* @return \Illuminate\Http\Response
* @return Response
*/
public function destroy(Category $category): JsonResponse
{
@@ -237,6 +247,7 @@ class CategoriesController extends Controller
);
} catch (\Exception $e) {
report($e);
return response()->json(
Helper::formatStandardApiResponse('error', null, trans('general.something_went_wrong'))
);
@@ -245,15 +256,15 @@ class CategoriesController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/categories/message.delete.success')));
}
/**
* Gets a paginated collection for the select2 menus
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0.16]
* @see \App\Http\Transformers\SelectlistTransformer
* @see SelectlistTransformer
*/
public function selectlist(Request $request, $category_type = 'asset') : array
public function selectlist(Request $request, $category_type = 'asset'): array
{
$this->authorize('view.selectlists');
$categories = Category::select([
@@ -263,7 +274,7 @@ class CategoriesController extends Controller
]);
if ($request->filled('search')) {
$categories = $categories->where('name', 'LIKE', '%'.$request->get('search').'%');
$categories = $categories->where('name', 'LIKE', '%'.$request->input('search').'%');
}
$categories = $categories->where('category_type', $category_type)->orderBy('name', 'ASC')->paginate(50);

View File

@@ -8,9 +8,9 @@ use App\Exceptions\AssetNotRequestable;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;
use Exception;
class CheckoutRequest extends Controller
{
@@ -18,6 +18,7 @@ class CheckoutRequest extends Controller
{
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'));
@@ -25,6 +26,7 @@ class CheckoutRequest extends Controller
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')));
}
}
@@ -33,11 +35,13 @@ class CheckoutRequest extends Controller
{
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

@@ -4,13 +4,13 @@ namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Transformers\CompaniesTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Company;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class CompaniesController extends Controller
{
@@ -18,9 +18,10 @@ class CompaniesController extends Controller
* Display a listing of the resource.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*/
public function index(Request $request) : JsonResponse | array
public function index(Request $request): JsonResponse|array
{
$this->authorize('view', Company::class);
@@ -38,16 +39,16 @@ class CompaniesController extends Controller
'accessories_count',
'consumables_count',
'components_count',
'tag_color',
'notes',
];
$companies = Company::withCount(['assets as assets_count' => function ($query) {
$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');
if ($request->filled('search')) {
$companies->TextSearch($request->input('search'));
}
@@ -56,7 +57,7 @@ class CompaniesController extends Controller
$companies->where('name', '=', $request->input('name'));
}
if ($request->filled('email')) {
if ($request->filled('email')) {
$companies->where('email', '=', $request->input('email'));
}
@@ -64,12 +65,15 @@ class CompaniesController extends Controller
$companies->where('created_by', '=', $request->input('created_by'));
}
if ($request->filled('tag_color')) {
$companies->where('tag_color', '=', $request->input('tag_color'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $companies->count()) ? $companies->count() : app('api_offset_value');
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort_override = $request->input('sort');
$sort_override = $request->input('sort');
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'created_at';
switch ($sort_override) {
@@ -84,25 +88,25 @@ class CompaniesController extends Controller
$total = $companies->count();
$companies = $companies->skip($offset)->take($limit)->get();
return (new CompaniesTransformer)->transformCompanies($companies, $total);
}
/**
* Store a newly created resource in storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @param \App\Http\Requests\ImageUploadRequest $request
*/
public function store(ImageUploadRequest $request) : JsonResponse
public function store(ImageUploadRequest $request): JsonResponse
{
$this->authorize('create', Company::class);
$company = new Company;
$company->fill($request->all());
$company = $request->handleImages($company);
if ($company->save()) {
return response()->json(Helper::formatStandardApiResponse('success', (new CompaniesTransformer)->transformCompany($company), trans('admin/companies/message.create.success')));
}
@@ -115,28 +119,31 @@ class CompaniesController extends Controller
* Display the specified resource.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*
* @param int $id
*/
public function show($id) : array
public function show($id): array
{
$this->authorize('view', Company::class);
$company = Company::findOrFail($id);
$this->authorize('view', $company);
return (new CompaniesTransformer)->transformCompany($company);
}
/**
* Update the specified resource in storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @param \App\Http\Requests\ImageUploadRequest $request
*
* @param int $id
*/
public function update(ImageUploadRequest $request, $id) : JsonResponse
public function update(ImageUploadRequest $request, $id): JsonResponse
{
$this->authorize('update', Company::class);
$company = Company::findOrFail($id);
@@ -157,10 +164,12 @@ class CompaniesController extends Controller
* Remove the specified resource from storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*
* @param int $id
*/
public function destroy($id) : JsonResponse
public function destroy($id): JsonResponse
{
$this->authorize('delete', Company::class);
$company = Company::findOrFail($id);
@@ -168,7 +177,7 @@ class CompaniesController extends Controller
if (! $company->isDeletable()) {
return response()
->json(Helper::formatStandardApiResponse('error', null, trans('admin/companies/message.assoc_users')));
->json(Helper::formatStandardApiResponse('error', null, trans('admin/companies/message.assoc_users')));
}
$company->delete();
@@ -180,10 +189,11 @@ class CompaniesController extends Controller
* Gets a paginated collection for the select2 menus
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0.16]
* @see \App\Http\Transformers\SelectlistTransformer
* @see SelectlistTransformer
*/
public function selectlist(Request $request) : array
public function selectlist(Request $request): array
{
$this->authorize('view.selectlists');
$companies = Company::select([
@@ -191,11 +201,11 @@ class CompaniesController extends Controller
'companies.name',
'companies.email',
'companies.image',
'companies.tag_color',
]);
if ($request->filled('search')) {
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->get('search').'%');
$companies = $companies->where('companies.name', 'LIKE', '%'.$request->input('search').'%');
}
$companies = $companies->orderBy('name', 'ASC')->paginate(50);

View File

@@ -2,20 +2,20 @@
namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedIn;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\ComponentsTransformer;
use App\Models\Component;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use App\Events\CheckoutableCheckedIn;
use App\Http\Transformers\ComponentsTransformer;
use App\Models\Asset;
use Illuminate\Support\Facades\Validator;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use App\Models\Component;
use Carbon\Carbon;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class ComponentsController extends Controller
{
@@ -23,16 +23,16 @@ class ComponentsController extends Controller
* Display a listing of the resource.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
*
* @since [v4.0]
*/
public function index(Request $request) : JsonResponse | array
public function index(Request $request): JsonResponse|array
{
$this->authorize('view', Component::class);
// This array is what determines which fields should be allowed to be sorted on ON the table itself, no relations
// Relations will be handled in query scopes a little further down.
$allowed_columns =
$allowed_columns =
[
'id',
'name',
@@ -58,8 +58,8 @@ 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', 'supplier', 'adminuser', 'manufacturer')
->withSum('unconstrainedAssets as sum_unconstrained_assets', 'components_assets.assigned_qty');
$filter = [];
@@ -78,7 +78,6 @@ class ComponentsController extends Controller
$components->TextSearch($request->input('search'));
}
if ($request->filled('name')) {
$components->where('name', '=', $request->input('name'));
}
@@ -87,6 +86,10 @@ class ComponentsController extends Controller
$components->where('components.company_id', '=', $request->input('company_id'));
}
if ($request->filled('order_number')) {
$components->where('components.order_number', '=', $request->input('order_number'));
}
if ($request->filled('category_id')) {
$components->where('category_id', '=', $request->input('category_id'));
}
@@ -108,15 +111,16 @@ class ComponentsController extends Controller
}
if ($request->filled('notes')) {
$components->where('notes','=',$request->input('notes'));
$components->where('notes', '=', $request->input('notes'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $components->count()) ? $components->count() : app('api_offset_value');
$components_count = $components->count();
$offset = ($request->input('offset') > $components_count) ? $components_count : app('api_offset_value');
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort_override = $request->input('sort');
$sort_override = $request->input('sort');
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'created_at';
switch ($sort_override) {
@@ -143,21 +147,20 @@ class ComponentsController extends Controller
break;
}
$total = $components->count();
$total = $components_count;
$components = $components->skip($offset)->take($limit)->get();
return (new ComponentsTransformer)->transformComponents($components, $total);
}
/**
* Store a newly created resource in storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @param \App\Http\Requests\ImageUploadRequest $request
*/
public function store(ImageUploadRequest $request) : JsonResponse
public function store(ImageUploadRequest $request): JsonResponse
{
$this->authorize('create', Component::class);
$component = new Component;
@@ -175,9 +178,10 @@ class ComponentsController extends Controller
* Display the specified resource.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @param int $id
*/
public function show($id) : array
public function show($id): array
{
$this->authorize('view', Component::class);
$component = Component::findOrFail($id);
@@ -191,17 +195,17 @@ class ComponentsController extends Controller
* Update the specified resource in storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
* @param \App\Http\Requests\ImageUploadRequest $request
*
* @param int $id
*/
public function update(ImageUploadRequest $request, $id) : JsonResponse
public function update(ImageUploadRequest $request, $id): JsonResponse
{
$this->authorize('update', Component::class);
$component = Component::findOrFail($id);
$component->fill($request->all());
$component = $request->handleImages($component);
if ($component->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $component, trans('admin/components/message.update.success')));
@@ -214,17 +218,19 @@ class ComponentsController extends Controller
* Remove the specified resource from storage.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v4.0]
*
* @param int $id
*/
public function destroy($id) : JsonResponse
public function destroy($id): JsonResponse
{
$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')));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.delete.error_qty')));
}
$component->delete();
@@ -236,35 +242,36 @@ class ComponentsController extends Controller
* Display all assets attached to a component
*
* @author [A. Bergamasco] [@vjandrea]
*
* @since [v4.0]
* @param Request $request
* @param int $id
*/
public function getAssets(Request $request, $id) : array
*
* @param int $id
*/
public function getAssets(Request $request, $id): array
{
$this->authorize('view', \App\Models\Asset::class);
$this->authorize('view', Asset::class);
$component = Component::findOrFail($id);
$offset = request('offset', 0);
$limit = $request->input('limit', 50);
if ($request->filled('search')) {
$assets = $component->assets()
->where(function ($query) use ($request) {
$search_str = '%' . $request->input('search') . '%';
$query->where('name', 'like', $search_str)
->orWhereIn('model_id', function (Builder $query) use ($request) {
$search_str = '%' . $request->input('search') . '%';
$query->selectRaw('id')->from('models')->where('name', 'like', $search_str);
})
->orWhere('asset_tag', 'like', $search_str);
})
->get();
->where(function ($query) use ($request) {
$search_str = '%'.$request->input('search').'%';
$query->where('name', 'like', $search_str)
->orWhereIn('model_id', function (Builder $query) use ($request) {
$search_str = '%'.$request->input('search').'%';
$query->selectRaw('id')->from('models')->where('name', 'like', $search_str);
})
->orWhere('asset_tag', 'like', $search_str);
})
->get();
$total = $assets->count();
} else {
$assets = $component->assets();
$total = $assets->count();
$assets = $assets->skip($offset)->take($limit)->get();
}
@@ -272,28 +279,28 @@ class ComponentsController extends Controller
return (new ComponentsTransformer)->transformCheckedoutComponents($assets, $total);
}
/**
* Validate and checkout the component.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* t
*
* @since [v5.1.8]
* @param Request $request
* @param int $componentId
*
* @param int $componentId
*/
public function checkout(Request $request, $componentId) : JsonResponse
public function checkout(Request $request, $componentId): JsonResponse
{
// Check if the component exists
if (!$component = Component::find($componentId)) {
if (! $component = Component::find($componentId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.does_not_exist')));
}
$this->authorize('checkout', $component);
$validator = Validator::make($request->all(), [
'assigned_to' => 'required|exists:assets,id',
'assigned_qty' => "required|numeric|min:1|digits_between:1,".$component->numRemaining(),
'assigned_to' => 'required|exists:assets,id',
'assigned_qty' => 'required|numeric|min:1|digits_between:1,'.$component->numRemaining(),
]);
if ($validator->fails()) {
@@ -302,11 +309,11 @@ class ComponentsController extends Controller
}
// Make sure there is at least one available to checkout
if ($component->numRemaining() < $request->get('assigned_qty')) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.checkout.unavailable', ['remaining' => $component->numRemaining(), 'requested' => $request->get('assigned_qty')])));
if ($component->numRemaining() < $request->input('assigned_qty')) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.checkout.unavailable', ['remaining' => $component->numRemaining(), 'requested' => $request->input('assigned_qty')])));
}
if ($component->numRemaining() >= $request->get('assigned_qty')) {
if ($component->numRemaining() >= $request->input('assigned_qty')) {
$asset = Asset::find($request->input('assigned_to'));
$component->assigned_to = $request->input('assigned_to');
@@ -314,29 +321,28 @@ class ComponentsController extends Controller
$component->assets()->attach($component->id, [
'component_id' => $component->id,
'created_at' => Carbon::now(),
'assigned_qty' => $request->get('assigned_qty', 1),
'assigned_qty' => $request->input('assigned_qty', 1),
'created_by' => auth()->id(),
'asset_id' => $request->get('assigned_to'),
'note' => $request->get('note'),
'asset_id' => $request->input('assigned_to'),
'note' => $request->input('note'),
]);
$component->logCheckout($request->input('note'), $asset);
$component->logCheckout($request->input('note'), $asset, null, [], $request->get('assigned_qty', 1));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkout.success')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkout.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.checkout.unavailable', ['remaining' => $component->numRemaining(), 'requested' => $request->get('assigned_qty')])));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/components/message.checkout.unavailable', ['remaining' => $component->numRemaining(), 'requested' => $request->input('assigned_qty')])));
}
/**
* Validate and store checkin data.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
*
* @since [v5.1.8]
* @param Request $request
* @param $component_asset_id
*/
public function checkin(Request $request, $component_asset_id) : JsonResponse
public function checkin(Request $request, $component_asset_id): JsonResponse
{
if ($component_assets = DB::table('components_assets')->find($component_asset_id)) {
if (is_null($component = Component::find($component_assets->component_id))) {
@@ -348,15 +354,15 @@ class ComponentsController extends Controller
$max_to_checkin = $component_assets->assigned_qty;
$validator = Validator::make($request->all(), [
"checkin_qty" => "required|numeric|between:1,$max_to_checkin"
'checkin_qty' => "required|numeric|between:1,$max_to_checkin",
]);
if ($validator->fails()) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Checkin quantity must be between 1 and ' . $max_to_checkin));
return response()->json(Helper::formatStandardApiResponse('error', null, 'Checkin quantity must be between 1 and '.$max_to_checkin));
}
// Validation passed, so let's figure out what we have to do here.
$qty_remaining_in_checkout = ($component_assets->assigned_qty - (int)$request->input('checkin_qty', 1));
$qty_remaining_in_checkout = ($component_assets->assigned_qty - (int) $request->input('checkin_qty', 1));
// We have to modify the record to reflect the new qty that's
// actually checked out.
@@ -376,10 +382,9 @@ class ComponentsController extends Controller
event(new CheckoutableCheckedIn($component, $asset, auth()->user(), $request->input('note'), Carbon::now()));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkin.success')));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/components/message.checkin.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, 'No matching checkouts for that component join record'));
}
}

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