Compare commits

...

559 Commits

Author SHA1 Message Date
snipe
cad6cc3007 Renamed the test for consistency
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 13:02:01 +01:00
snipe
b303875f1d Merge pull request #17734 from grokability/#17726-add-welcome-email-to-new-user-form
Fixed #17726: add welcome email to new user form
2025-08-28 07:29:56 +01:00
snipe
d5cc61f378 Added send to API call for creating users
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 07:28:51 +01:00
snipe
0d7ec43262 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:19:41 +01:00
snipe
d3747f4daa Added welcome email to controller
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:12:01 +01:00
snipe
af695e7dc8 Added help to user importer
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:11:52 +01:00
snipe
1edbfd87df Added welcome email checkbox to user create form
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:11:40 +01:00
snipe
454be01a6c Updated translations
Signed-off-by: snipe <snipe@snipe.net>
2025-08-28 06:11:23 +01:00
snipe
745fc515f1 Merge pull request #17713 from Godmartinz/fix-localization-for-email-notifications
Adds #5554 locale for acceptance notifications and checkin/out emails
2025-08-28 05:29:28 +01:00
snipe
715b9c1182 Merge pull request #17730 from Godmartinz/update-asset-accepance-with-category
Adds #9000 Item type to Account Asset Acceptance index
2025-08-28 05:22:54 +01:00
Godfrey M
95be847d87 renamed attribute 2025-08-27 14:10:51 -07:00
Godfrey M
c1a6546eba change column header 2025-08-27 14:09:13 -07:00
Godfrey M
648c25a0a7 adds item type to Accept asset index 2025-08-27 14:06:10 -07:00
Godfrey M
f2ec7f2975 fix tests 2025-08-27 13:22:35 -07:00
Godfrey M
f518af6d61 fix class name 2025-08-27 13:09:05 -07:00
snipe
b11c6a5c06 Updated depreciation translation with more information.
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 16:35:49 +01:00
snipe
5822e4e692 Merge pull request #17729 from grokability/exit-early-if-ldap-troubleshooter-cannot-decrypt-ldap-pw
Put LDAP troubleshooter's decrypt in a try/catch to avoid crashing if it cannot decrypt the password
2025-08-27 15:47:22 +01:00
snipe
e4f06b0ca8 One last time
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 15:43:48 +01:00
snipe
2f093c0e82 Added early exist on step 4 as well
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 15:41:39 +01:00
snipe
5d9dc0e74d Put decrypt in a try/catch
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 15:33:26 +01:00
snipe
adc3a34929 Fixed copy for encrypted custom fields
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 14:38:36 +01:00
snipe
cb2ffe6b3f Updated translations
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 14:02:39 +01:00
snipe
b3e3d01672 Fixed LDAP icon spacing
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 13:32:02 +01:00
snipe
4a6520fc78 Fixed address field
Signed-off-by: snipe <snipe@snipe.net>
2025-08-27 13:30:07 +01:00
snipe
75ab6c9b13 Merge pull request #17723 from uberbrady/improve_ldap_certificate_ignoring
Improve ldap certificate ignoring
2025-08-27 13:29:33 +01:00
snipe
2f77fcb526 Merge pull request #17724 from Godmartinz/checkout2location_email_fix
Fixes #17642 Checkouts to location email for Assets and Accessories
2025-08-27 13:02:57 +01:00
Brady Wetherington
60604c3481 With the new SSL stuff, we are calling ldap_set_option() one more time now 2025-08-27 12:25:39 +01:00
Godfrey M
671c113cd2 add coma to translation" 2025-08-26 16:07:04 -07:00
Godfrey M
8a74d21ede fixes checkout emails to location for assets and accessories" 2025-08-26 16:00:26 -07:00
Godfrey M
75995b2109 fix checkout to location email 2025-08-26 15:34:38 -07:00
snipe
d1eefc3fea Merge pull request #17692 from grokability/#17387-make-saml-key-size-an-env
Fixed #17386 - Added SAML key size to env - possible alternative to #17387
2025-08-26 16:28:27 +01:00
Brady Wetherington
16795382fc Many cleanups to default-mode of LDAP troubleshooter 2025-08-26 15:53:18 +01:00
snipe
eb17974adc Merge pull request #17722 from grokability/#17704-retain-linebreaks
Fixed #17704 -  retain linebreaks on custom field clipboard copy
2025-08-26 15:44:38 +01:00
snipe
22852c27f8 Use generic length for asterisks
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:38:56 +01:00
snipe
f4a94d975d Fixes #17704 - retain linebreaks in clipboard for multi-line custom field copying
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:33:19 +01:00
snipe
7a36bbbd1e Merge pull request #17721 from grokability/small-ldap-preview-display-tweaks
Improved LDAP field sync preview
2025-08-26 15:26:09 +01:00
snipe
2b401b965b Fixed casing
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:22:00 +01:00
snipe
314bc5b44f Added manager
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:14:29 +01:00
snipe
76374f0d5a Updated text
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:14:22 +01:00
snipe
264efb015e Fixed jobtitle field mapping
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 15:05:05 +01:00
Brady Wetherington
e74460aefc Merge branch 'develop' into improve_ldap_certifcate_ignoring 2025-08-26 15:01:11 +01:00
Brady Wetherington
55a5a12b30 Formalize the 'double-barrel' method of setting TLS cert ignores 2025-08-26 15:00:33 +01:00
snipe
58944a38eb Make screen and table wider
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 14:59:11 +01:00
snipe
469e3bd475 Nicer ldap preview layout, show all mapped fields
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 14:51:34 +01:00
snipe
17650c5735 Changed field title
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 14:03:11 +01:00
Brady Wetherington
15e64155b5 Add version checking to LDAP troubleshooter, clean up ldap model 2025-08-26 13:57:25 +01:00
snipe
39955ac760 Add @akaspeh1 as a contributor 2025-08-26 12:42:20 +01:00
snipe
855a176ca9 Add @nickwest as a contributor 2025-08-26 12:42:15 +01:00
snipe
47b2b30455 Merge pull request #17710 from akaspeh1/develop
Adds support for label sheets Avery L4736 & L6009
2025-08-26 12:42:02 +01:00
snipe
b702e3e2de Merge pull request #17492 from ischooluw/17448-feature-notes-api-endpoints
Fixes #17448: feat(api) - API endpoint for Adding Ad-Hoc Notes to Assets
2025-08-26 12:40:52 +01:00
snipe
a6b74d56c6 Merge pull request #17709 from grokability/add-display-name-to-users-fixed
Added display name to users for LDAP/SCIM, added new sync fields (replaced #17650)
2025-08-26 12:39:25 +01:00
snipe
a4222bcaef Merge pull request #17711 from grokability/dependabot/github_actions/develop/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-08-26 12:10:24 +01:00
snipe
ecf24511cd Fixed tests for real this time tho
Signed-off-by: snipe <snipe@snipe.net>
2025-08-26 12:09:55 +01:00
snipe
abb097a391 Merge pull request #17714 from Godmartinz/Audit_null_fix
Added null checks to MS Teams Audit notification
2025-08-26 10:44:51 +01:00
Godfrey M
dd742a2e4a add a check for audit notification variables in MS Teams and a translation 2025-08-25 15:10:41 -07:00
Godfrey M
128bdf500a sends an email for to locale and cc locale 2025-08-25 12:02:23 -07:00
dependabot[bot]
73ac00bc51 Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [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/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 16:25:39 +00:00
snipe
3524e23e38 Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-25 17:17:45 +01:00
Jakub Aqaba Štarman
be0f3910bb Fixed: Old computation 2025-08-25 16:57:32 +02:00
snipe
07dbc6842c Are you KIDDING ME, Github??
This reverts commit c8e79aa5ca, reversing
changes made to e60f2b2332.

Signed-off-by: snipe <snipe@snipe.net>
2025-08-25 15:56:28 +01:00
Jakub Aqaba Štarman
5a16b59462 Adds support for label sheets Avery L4736 & L6009 2025-08-25 16:47:52 +02:00
Brady Wetherington
13cd7071b8 WIP improving some LDAP stuff 2025-08-25 15:41:01 +01:00
snipe
40108b196c Trying to fix import tests :(
Signed-off-by: snipe <snipe@snipe.net>
2025-08-25 15:28:43 +01:00
snipe
c8e79aa5ca Merge branch 'develop' into add-display-name-to-users-fixed 2025-08-25 15:28:20 +01:00
snipe
e60f2b2332 Tightened up accessor code for better inheritence
Signed-off-by: snipe <snipe@snipe.net>
2025-08-25 15:00:10 +01:00
snipe
b6d397bcca Updated ->present()->fullName() to ->display_name
Signed-off-by: snipe <snipe@snipe.net>
2025-08-25 14:57:34 +01:00
snipe
6503f9c667 Revert "Merge pull request #17650 from grokability/add-displayName-to-users"
This reverts commit 4770e469b4, reversing
changes made to 29a18c7c8b.

Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 20:23:47 +01:00
snipe
4770e469b4 Merge pull request #17650 from grokability/add-displayName-to-users
Add display name to users for LDAP/SCIM, added new sync fields
2025-08-21 18:22:34 +01:00
snipe
29a18c7c8b Merge pull request #17696 from uberbrady/add_created_at_index_to_models
Fixed [FD-49550] - added a 'created_at' index to the models table
2025-08-21 14:54:20 +01:00
Brady Wetherington
6db0003e3f Adds a 'created_at' index to the models table 2025-08-21 13:44:14 +01:00
snipe
c538c460fa Merge pull request #17695 from grokability/#17482-better-localization-indates-on-asset-view
Use nicer local for purchase date
2025-08-21 13:13:26 +01:00
snipe
822339fe42 Moved warning
Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 13:13:11 +01:00
snipe
b84d9282ca Use normal locale for warranty
Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 13:05:01 +01:00
snipe
952b6f33bb Add @strobelm as a contributor 2025-08-21 11:51:37 +01:00
snipe
c57c4b8ff2 Merge pull request #17691 from qay21/fix-components-url
Fix components presenting wrong URLs
2025-08-21 11:37:27 +01:00
snipe
39e6223ff2 POssible alternative to #17386 - adding SAML key size to env
Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 11:27:50 +01:00
qay
d8dd274c08 Fix components presenting wrong URLs 2025-08-21 12:26:13 +02:00
snipe
15f97b6cb9 Merge pull request #17591 from Godmartinz/add-serial-to-expiring-asset-report
Adds #17440 serial number column to Expiring Assets Report
2025-08-21 11:14:45 +01:00
snipe
fc091c1174 Added comments
Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 09:29:12 +01:00
snipe
c07ef4d87f A few small tweaks
Signed-off-by: snipe <snipe@snipe.net>
2025-08-21 09:25:42 +01:00
snipe
11eee833bb Fixed #17667 - Switch to hyphens for windows
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 15:56:10 +01:00
snipe
fec9d716ee Merge pull request #17679 from grokability/#17674-add-ods-and-odt
Fixed #17674: added .ods, .odp, and .odt as acceptable upload types
2025-08-20 14:17:08 +01:00
snipe
da5b1afd19 Removed logging
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 14:11:42 +01:00
snipe
618106c103 Fixed #17674 - added odp, ods, odt to accepted files
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 14:11:20 +01:00
snipe
312be98132 Add @FlorestanII as a contributor 2025-08-20 12:43:43 +01:00
snipe
e0bb77a6d6 Merge pull request #17664 from FlorestanII/feature/support-for-dymo-11354-labels
Support for Dymo 11354 Labels.
2025-08-20 12:43:29 +01:00
snipe
855922c21a Account for null in tetss (vs 0)
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 11:32:16 +01:00
snipe
bc645d2621 Use email formatter in licensed_to_email display
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 11:24:16 +01:00
snipe
9c06ff3899 Check for numeric
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 11:00:18 +01:00
snipe
2a37aa3b49 Fixed tooltip
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 10:34:05 +01:00
snipe
bf591320af Fixed #17665 - delete custom report modal
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 09:58:30 +01:00
snipe
56e687bed2 Retuen the display name in the API call
Signed-off-by: snipe <snipe@snipe.net>
2025-08-20 09:33:00 +01:00
snipe
07b25fe376 Add display name to summary
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 20:52:18 +01:00
snipe
c2ecd20b7d Updated field text
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 20:47:48 +01:00
snipe
1b42abcc98 Fixed mapping
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 19:54:32 +01:00
snipe
9efb49d510 Merge pull request #17663 from Godmartinz/sub-out-translation
Fixes #17653 changes translation to administrator
2025-08-19 19:43:47 +01:00
snipe
2d6270c697 Updated validation, switch to string() as db field type
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 19:19:29 +01:00
snipe
0823c23a6e Fixed placeholder text
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 18:51:56 +01:00
snipe
b3f0ce4b2a Use fieldsets for LDAP settings
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 18:38:47 +01:00
snipe
8b83584b67 Added mapping fields to LDAP
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 18:31:58 +01:00
Godfrey M
9eb686fe08 changes translation to administrator 2025-08-19 10:23:15 -07:00
Johannes Pollitt
765051ce88 Added LabelWriter for 11354 format labels.
Printable for example with the Dymo LabelWriter 450.
2025-08-19 19:21:48 +02:00
Godfrey M
ed402e0122 adds serial underneath name 2025-08-19 10:10:20 -07:00
snipe
1488271a83 Added #8522 - depreciation info on Asset API
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 14:48:48 +01:00
snipe
48bbf8d005 Merge pull request #17655 from uberbrady/add_category_indexes
Add new indexes to category_id and deleted_at
2025-08-19 14:26:38 +01:00
Brady Wetherington
e97b969d66 Add new indexes to category_id and deleted_at 2025-08-19 14:20:36 +01:00
snipe
cdd12df81a Fixed #17627 - jquery UI fix for draggable/sortable
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 14:12:06 +01:00
snipe
050a3afc74 Fixed #17649 - nicer layout on new location modal
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 13:56:21 +01:00
snipe
270401c693 Added display name to user create modal
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 13:12:57 +01:00
snipe
551822ce7d Merge pull request #17648 from grokability/possible-fix-for-#17641-map-mobile-via-scim
Fixed #17641: map mobile number via SCIM
2025-08-19 13:09:07 +01:00
snipe
4b8c371097 Updated true to false
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 12:59:28 +01:00
snipe
90fbf6da46 Modify the presenter to see if they have a display_name set
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 12:56:44 +01:00
snipe
0c3103e3d2 Modify the getter
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 12:56:30 +01:00
snipe
6a8e1566fe Added display_name to a few more places
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 12:56:11 +01:00
snipe
ced30082a6 Added display_name as user field
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 12:10:28 +01:00
snipe
f6c64abc1a Fixed #17641 - map mobile number via SCIM
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 11:41:02 +01:00
snipe
7f9939a896 Merge pull request #17638 from Godmartinz/asset-tag-added-to-subject-line
Adds asset tag to subject line of check in check out
2025-08-19 09:36:25 +01:00
Godfrey M
1c99f2dfdd readd doesntorequireacceptance() to test 2025-08-18 10:52:35 -07:00
Godfrey M
1974fccac3 add tag to other notification test 2025-08-18 10:48:39 -07:00
Godfrey M
911552035e fix other test 2025-08-18 10:39:10 -07:00
Godfrey M
ff25d275ee fix tests 2025-08-18 10:31:03 -07:00
Godfrey M
1fcf5e03e7 adds asset tag to subject line of checkin/out 2025-08-18 10:16:47 -07:00
snipe
9b4101855f Undo double-float
Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 15:24:15 +01:00
snipe
9253d894d3 Removed XSS-Protection header
@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-XSS-Protection#security_considerations

Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 13:30:53 +01:00
snipe
ebd79f22c7 Merge pull request #17636 from grokability/#17627-custom-fields-sorting
Fixed #17627: custom fields not sorting correctly
2025-08-18 12:47:03 +01:00
snipe
c1b139fb9a Fixed #17627: custom fields not sorting correctly
Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 12:31:03 +01:00
snipe
a88bcea8ca Merge pull request #17635 from grokability/#17367-fixed-padlock-icon
Fixed #17367: Small adjustment to css-padlock
2025-08-18 11:25:55 +01:00
snipe
21566560a7 Fixed #17367: Small adjustment to css-padlock
Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 11:24:05 +01:00
snipe
e3ca43bf40 Remove use of formatCurrencyOutput for input display
Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 11:00:19 +01:00
snipe
61abb8d5cb Fixed hardware.bulkedit redirect
Signed-off-by: snipe <snipe@snipe.net>
2025-08-18 09:45:02 +01:00
snipe
ecad656551 Merge pull request #17626 from grokability/#17606-s3-url-for-models-on-requestable-view
Fixed #17606 - use `getImageUrl()` to determine if local or S3
2025-08-17 14:54:13 +01:00
snipe
615e6d6e4f Fixes #17606 - use getImageUrl() to determine if local or S3
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 14:51:52 +01:00
snipe
6dceefb96e Merge pull request #17625 from grokability/#17620-delete-method-custom-fields
Fixed #17620 - delete method custom fields causing method not allowed error
2025-08-17 14:11:17 +01:00
snipe
69eff394fd Removed use statement
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 14:06:56 +01:00
snipe
a9da3aca81 Combine fields and fieldset exception
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 14:06:49 +01:00
snipe
91f3556375 Added delete test
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:33:53 +01:00
snipe
aab7c3a840 Updated custom fields and fieldset pages to use standard delete modal
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:33:47 +01:00
snipe
9c823119e3 Added new factories for user custom field permissions
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:31:14 +01:00
snipe
f5128833f6 Updated comments
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:30:52 +01:00
snipe
2bc144354a Use translations and more standard error bag
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:30:43 +01:00
snipe
e6fec6ec34 Trim model name for display
Signed-off-by: snipe <snipe@snipe.net>
2025-08-17 13:30:28 +01:00
snipe
53389875bf Merge pull request #17611 from grokability/#9965-fallback-to-category-image-for-consumables
Fixed #9965 - fallback to category images (f there are any) when no c…
2025-08-15 15:07:13 +02:00
snipe
3b243b38c8 Fixed #9965 - fallback to category images (f there are any) when no consumable image is present
Signed-off-by: snipe <snipe@snipe.net>
2025-08-15 15:03:09 +02:00
snipe
3d9580808b Merge pull request #17524 from Godmartinz/add-category-and-model-to-checkout-emial
Adds #17507 Category and Model No. to accessory checkout markdown
2025-08-15 14:39:58 +02:00
snipe
2141ee71d4 Merge pull request #17544 from marcusmoore/fixes/custom-field-filter
Fixed invalid custom fields being used for filtering
2025-08-15 14:39:09 +02:00
snipe
01dd07083e Merge pull request #17584 from spencerrlongg/bug/17312-custom-field-checkbox-will-not-clear-if-no-checkboxes-should-be-selected
Fixed #17312 - Fix Nulling Checkboxes
2025-08-15 14:35:37 +02:00
snipe
42a28ea06b Merge pull request #17593 from Godmartinz/add-admin-to-acceptance-emails
FIXED #17380 Adds Admin name to acceptance emails
2025-08-15 14:33:02 +02:00
snipe
180cb6ba8e Merge pull request #17610 from grokability/#17600-add-checkout-date-to-accessory-list
Fixed #17600 - adds checkout date to accessories tab in user view
2025-08-15 14:31:38 +02:00
snipe
a78762e40b Fixed #17600 - adds checkout date to accessories tab in user view
Signed-off-by: snipe <snipe@snipe.net>
2025-08-15 14:29:55 +02:00
snipe
9797bb19e2 Updated dev assets
Signed-off-by: snipe <snipe@snipe.net>
2025-08-15 14:23:22 +02:00
snipe
08a9554b3c Merge pull request #17607 from Godmartinz/color-corrections-pt9000
Fixes #17488 more info text colors
2025-08-14 20:39:26 +02:00
Godfrey M
d79bd825ee fix popover text color 2025-08-14 10:51:31 -07:00
Godfrey M
fe3d225cfa fix tests 2025-08-14 09:15:19 -07:00
snipe
376e0db66e Merge pull request #17601 from ubc-cpsc/bugfix/CVE-2025-55166
Fixes CVE-2025-55166
2025-08-13 20:49:41 +02:00
Joël Pittet
5fdabc1a62 Fixes CVE-2025-55166 2025-08-13 11:42:14 -07:00
Godfrey M
dfe2a75d72 adds user that checked out item to acceptance emails 2025-08-12 15:34:46 -07:00
Godfrey M
ba85af11aa adds serial to expiring assets report email 2025-08-12 14:59:20 -07:00
Godfrey M
db58b80d27 Merge branch 'develop' into add-category-and-model-to-checkout-emial
# Conflicts:
#	app/Mail/CheckoutLicenseMail.php
2025-08-12 14:20:08 -07:00
Godfrey M
5cb8aae383 add ternaries 2025-08-12 14:16:46 -07:00
spencerrlongg
817530429b added condition to make sure the request has checkbox 2025-08-12 14:52:52 -05:00
Marcus Moore
4a7b7183d2 Add custom_fields. prefix so custom fields can be filtered against 2025-08-11 14:58:41 -07:00
snipe
94bd39cf23 Merge pull request #17570 from grokability/#10038-add-active-flag-filter
Added sidenav to filter on activated vs inactive users
2025-08-11 20:45:22 +01:00
snipe
4038a22093 Added sidenav to filter on activated vs inactive users
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 20:41:55 +01:00
snipe
682baec0c9 Merge pull request #17569 from grokability/#10284-add-mobile-number
Fixed #10284: Added mobile phone to users
2025-08-11 18:49:49 +01:00
snipe
ff91be491d Added mobile to tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 18:43:37 +01:00
snipe
ef35a0f2f1 Fixed #10284: Added mobile phone to users
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 18:38:22 +01:00
snipe
f12a3bb08b Fixed #10306 - cast purchase cost to a float
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 18:12:37 +01:00
snipe
c8a5065ffa Merge pull request #17567 from grokability/#11754-nicer-menu-alignment
Fixed #11754: nicer menu alignment for dropdowns
2025-08-11 14:57:59 +01:00
snipe
23da5573f3 Fixed #11754 - nicer top menu dropdown alignment
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 14:56:43 +01:00
snipe
b08f985776 Merge pull request #17566 from grokability/partial-fix-for-#17565-standard-layout
Show all icons on location table, even if no results
2025-08-11 14:17:59 +01:00
snipe
9b968baaa7 Show all icons, even if no results
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 14:14:15 +01:00
snipe
07edbe6f1c Add @mckaygerhard as a contributor 2025-08-11 13:08:54 +01:00
snipe
1f55a8b6e3 Added icon and tooltip
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 13:06:37 +01:00
snipe
f6b9e11810 Merge pull request #17538 from mckaygerhard/mail-log-improvements
Mail log for #17491 and some improvements on log errors
2025-08-11 13:05:56 +01:00
snipe
c18a3e4266 Fixed #17562 - bootstrap table formater undefined
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 11:18:20 +01:00
snipe
5840ef1c6f Fixed #17560
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 06:26:15 +01:00
snipe
7974baddf5 Merge pull request #17551 from grokability/move-file-uploads-paths-to-base-controller
Move the object type mapping and such to the base controller to de-dupe
2025-08-11 05:44:39 +01:00
snipe
4bf569758f Cleans up a few rmore outes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-11 05:05:00 +01:00
snipe
f56fd9bb0b Bumped hash
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 21:04:33 +01:00
snipe
357ee5fc45 Copy over the old dirs just in case
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 21:02:37 +01:00
snipe
c6dea085b2 Missed a few
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 21:01:23 +01:00
snipe
8782c3ecec Merge pull request #17554 from grokability/#13997-add-ldap-sync-via-api
Adds #13997 - API endpoint to sync users via LDAP
2025-08-10 20:30:44 +01:00
snipe
b636cf2ef0 Merge pull request #17555 from grokability/#17490-use-numeric-for-purchase-cost
Fixed #17490: use numeric for purchase cost
2025-08-10 20:30:15 +01:00
snipe
6dee2b8601 Fixed 17490 - currency symbol breaks purchase_cost
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 19:04:52 +01:00
snipe
bcf301ac17 Adds #13997 - API endpoint to sync users via LDAP
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 18:48:01 +01:00
snipe
bf2120fb31 Use newer file path 2025-08-10 18:26:41 +01:00
snipe
de56b74f3e Merge branch 'develop' into move-file-uploads-paths-to-base-controller 2025-08-10 18:25:47 +01:00
snipe
2f146abe91 Let people upload images on the demo
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 18:20:35 +01:00
snipe
543d41b6ff Merge pull request #17553 from grokability/#17547-asset-model-images-not-loading
Fixed #17547: asset model images not loading
2025-08-10 18:15:57 +01:00
snipe
8da0dd7563 Use strtolower
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 18:11:39 +01:00
snipe
a2217d7dbc Specify the public disk for creating directories
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 18:08:15 +01:00
snipe
ea84728a3f Rename models uploads dir
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 17:58:11 +01:00
snipe
b2d10f7ccf Rename directory
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 17:56:59 +01:00
snipe
b6af25ce99 Fixed #17548 - treeview menu class on people menu
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 17:20:49 +01:00
snipe
7a9d2454d4 Move the object type mapping and such to the base controller to de-dupe
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 16:30:32 +01:00
snipe
a9254cff02 Merge pull request #17550 from grokability/addded-observer-for-maintenances
Added basic logging for maintenances
2025-08-10 16:00:49 +01:00
snipe
d14b34141c Updated comment
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 15:53:53 +01:00
snipe
14bc2cc1ba Added basic logging for maintenances
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 15:51:48 +01:00
snipe
a91b54b97a Added buttons to maintenances table
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 14:55:34 +01:00
snipe
ead655e1db Fixed translation
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 14:52:40 +01:00
snipe
c5f28748f7 Merge pull request #17549 from grokability/rename_title_to_name_for_maintenances
Renamed table from `asset_maintenances` to `maintenances` and `title` to `name` for maintenances
2025-08-10 14:28:51 +01:00
snipe
ee4831cb30 Removed followsRedirects so we can check the status
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 14:23:41 +01:00
snipe
deb1afd28b Few more replcamenents
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 14:14:21 +01:00
snipe
9e8eead71e Renamed routes and method names
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:29:48 +01:00
snipe
3f96f7cbd7 Updated file paths for uploads
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:24:45 +01:00
snipe
dde2e88332 Renamed variables in purge
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:24:32 +01:00
snipe
ff25015595 Renamed more files
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:24:14 +01:00
snipe
7d0c695808 Renamed language directories
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:23:52 +01:00
snipe
906385def9 Renamed directories
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:23:16 +01:00
snipe
a6c6c7eae9 Updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 13:11:50 +01:00
snipe
205725c767 Renamed model
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:30:50 +01:00
snipe
c207efbb35 Rename model in breadcrumbs
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:30:42 +01:00
snipe
c0211e59b3 Renames maintenances presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:30:23 +01:00
snipe
dd2678cbb9 Rename maintenances path
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:30:09 +01:00
snipe
e2c87b664e Rename factory
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:28:58 +01:00
snipe
29d4b4dd53 Updated API routes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:28:28 +01:00
snipe
3fba307e55 Updated routes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:28:18 +01:00
snipe
7171fa36d8 Added migrations
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:27:59 +01:00
snipe
c570f656bf Renamed test
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 12:27:48 +01:00
snipe
a5e37519f5 Merge pull request #17539 from grokability/add-file-uploads-to-maintenances
WIP: Add file uploads to maintenances
2025-08-10 11:13:19 +01:00
snipe
0f88d6eec3 Removed error logging
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 11:09:29 +01:00
snipe
651c51bb01 Remove unused statements
Signed-off-by: snipe <snipe@snipe.net>
2025-08-10 10:41:46 +01:00
mckaygerhard
0fdbdfd5c2 Improve log error handling regarding notification sending for issue #17491
* when an error is generated when denying checkouts, there are not enough logs
to determine the problem from the email provider
* missing handling of log test mail config, there is none of logs cos there
is no log handling on test email, becouse all the results are just sent to
the http response and no log were writen.
2025-08-08 12:18:04 -04:00
snipe
31056ff858 Added new dirs to restore tool
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:56:07 +01:00
snipe
8d2643696b Deleted unused user file controller
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:55:59 +01:00
snipe
e7488d19e9 Fixed name for uploaded files controller
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:55:48 +01:00
snipe
2bb3b6d64c Fixed prefixes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:55:24 +01:00
snipe
5744e48ae8 Added getDisplayNameAttribute() to maintenances
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:54:36 +01:00
snipe
82d0a21440 Added to actionlog model
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:54:09 +01:00
snipe
58133cffac Updated maintenance model
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:37:03 +01:00
snipe
bfd8c2f310 Added fles UI on maintenance page
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:36:51 +01:00
snipe
30d447c023 Updated urls/routes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:36:35 +01:00
snipe
9a0846b8a6 Added directory
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 12:36:16 +01:00
snipe
3667fcddd7 Mark flappy API rate limiting tests as skipped :(
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 11:37:36 +01:00
snipe
906741d662 Bumped debug to warning
Signed-off-by: snipe <snipe@snipe.net>
2025-08-08 11:32:04 +01:00
snipe
12be088c4f Merge pull request #17523 from Godmartinz/fix-create-new-rediret
Fixes #17457 Previous Page redirect option
2025-08-08 09:50:40 +01:00
snipe
6737ba80cd Merge pull request #17489 from grokability/fixes/#17485-handle-alert-menu-better-if-no-alerts
Fixed #17485: nicer alert menu if no items are below qty
2025-08-08 09:50:14 +01:00
snipe
862a3d938e Merge pull request #17543 from Godmartinz/salutation-target-fix
Salutation target fix
2025-08-08 09:49:24 +01:00
snipe
09e82377a5 Merge pull request #17520 from marcusmoore/missing-user-redirect-fix
Fixed potential failure in license checkin due to redirect option
2025-08-08 09:48:43 +01:00
snipe
59470864e7 Merge pull request #17542 from akemidx/assetpolicyclassimport
AssetPolicy class import
2025-08-08 09:40:23 +01:00
Marcus Moore
c95aeb3730 Filter out unallowed columns (custom fields) 2025-08-07 17:25:20 -07:00
Godfrey M
5c55c90d68 add null checks to target 2025-08-07 15:27:50 -07:00
Godfrey M
e47972731b fixed target name for checkouts with licenses and assets 2025-08-07 15:12:23 -07:00
Godfrey M
5851cc9e41 fixed target name for checkouts with licenses and assets 2025-08-07 15:09:38 -07:00
akemidx
6f615230e9 class import 2025-08-07 17:00:28 -04:00
snipe
d91598a25e Merge pull request #17540 from marcusmoore/fixes/asset-serial-validation
Fixed 500 when sending non-string for serial property
2025-08-07 20:53:07 +01:00
snipe
9e416778d9 Merge pull request #17541 from marcusmoore/remove-dump-in-test
Removed debugging dump() in test
2025-08-07 20:52:07 +01:00
Marcus Moore
860a117567 Remove dump in test 2025-08-07 12:50:02 -07:00
Marcus Moore
b8fe3b18d4 Add "string" to serial rules for asset 2025-08-07 12:27:48 -07:00
snipe
40269a724b Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:42:59 +01:00
snipe
ec828318d8 Merge pull request #17417 from marcusmoore/snipe-it-17073-asset-requests-are-not-deleted-when-asset-is-deleted
Fixed #17073 - delete old checkout requests
2025-08-07 18:32:13 +01:00
snipe
d31e7ed534 Use new route
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:24:02 +01:00
snipe
5c2dbe438b Added maintenances
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:23:57 +01:00
snipe
10857635ac Removed unused use statement
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:23:44 +01:00
snipe
df2545ef80 Updated routes
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:23:17 +01:00
snipe
f6ff729316 Added new generic files upload controller
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:22:57 +01:00
snipe
38678803eb Removed unused controllers
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 18:22:45 +01:00
snipe
67c931f196 Merge pull request #17080 from marcusmoore/allow-id-on-location-select
Allowed setting `id` on location-select component
2025-08-07 18:16:58 +01:00
snipe
1c23092d0e Merge pull request #17537 from grokability/add-maintenance-images-and-files
Fixed #10357: Add maintenance image upload
2025-08-07 17:02:34 +01:00
snipe
a90ff21cbf Cleaned up a few more tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 16:58:44 +01:00
snipe
0ce0cee81f Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 16:53:18 +01:00
Герхард PICCORO Lenz McKAY
f4be5ffb5d Fix workaround for #17491 log error on failed response for mail sending
* Part of bunch of fixes, this fix #17491 where admins at test install cannot see the log of errors for UI test mail button, we can just see that this is the correct form cos other parts of the code manage the exception inside the catch using log interface class
2025-08-07 11:42:17 -04:00
snipe
19958748bf Use image upload request
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:39:12 +01:00
snipe
d6ca8468e3 Use snake case for naming paths
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:39:01 +01:00
snipe
7bccb7718b Added partial and enctype="multipart/form-data for upload
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:38:22 +01:00
snipe
f6b63b5e44 Added image to view
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:38:04 +01:00
snipe
9a2c5ff195 Updated/added tests
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:37:57 +01:00
snipe
3597f759da Updated transformers and presenters
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:37:45 +01:00
snipe
3ed3b21286 Added maintenance file singleton
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:37:32 +01:00
snipe
b89b636474 Added migration
Signed-off-by: snipe <snipe@snipe.net>
2025-08-07 15:37:16 +01:00
snipe
2afc595452 Don’t show license key formatter if no value
Signed-off-by: snipe <snipe@snipe.net>
2025-08-06 16:47:47 +01:00
snipe
c7262f2885 Merge pull request #17532 from grokability/add-available-licenses-back-for-now
[FD-50162] Put remaining seats back on license view for now
2025-08-06 16:35:34 +01:00
snipe
8662aa2277 Put remaining seats back on license view for now
Signed-off-by: snipe <snipe@snipe.net>
2025-08-06 16:33:02 +01:00
snipe
8095e0ab72 Normalize consumables user response
Signed-off-by: snipe <snipe@snipe.net>
2025-08-06 16:25:51 +01:00
snipe
be3c8ddd5c Hotfix for FD-50160
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 23:19:27 +01:00
Godfrey M
ec5b9ce903 adds category and model no to accessory checkout markdown 2025-08-05 12:44:07 -07:00
Godfrey M
bd2acefecc rethought, keeping previous page as an option 2025-08-05 12:29:59 -07:00
Godfrey M
18e49e9067 only redirect to previous page if not creating 2025-08-05 12:05:22 -07:00
snipe
a0d65520a3 Use count() instead of ->count() for user count in print view
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 19:34:59 +01:00
snipe
a35731d9d5 Fixed #17513 - updated language string
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 19:06:08 +01:00
snipe
9d3623cca6 Merge pull request #17521 from grokability/#17518-add-break-after-sigs
Fixed #17518: Adds printer line break after signatures
2025-08-05 19:02:24 +01:00
snipe
2fe08a721f Do not break the page if it’s the last entry
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 19:00:57 +01:00
Marcus Moore
7abc3a7d7d Only push to session if user exists 2025-08-05 10:57:07 -07:00
snipe
d4a34f1a3c Adds printer line break after signatures
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 18:50:47 +01:00
snipe
ddda4848d3 Added avif to inline
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 18:13:17 +01:00
snipe
8516856d37 Merge pull request #17456 from spencerrlongg/9511-validation-always-fails-on-encrypted-custom-fields
Fixed #9511 - Validation For Encrypted Custom Fields
2025-08-05 17:45:38 +01:00
snipe
132327594b Merge pull request #17515 from grokability/add-submenu-to-users
Added dropdown menu for users
2025-08-04 22:26:59 +01:00
snipe
d2a2c63070 Added dropdown menu for users
Signed-off-by: snipe <snipe@snipe.net>
2025-08-04 22:25:23 +01:00
snipe
170a5158fa Merge pull request #17514 from grokability/images-on-cloning
Added ability to copy images on cloning
2025-08-04 21:04:56 +01:00
snipe
1d8493d388 Improved messaging for cloning/editing assets that inherit images
Signed-off-by: snipe <snipe@snipe.net>
2025-08-04 20:51:24 +01:00
Marcus Moore
ff39e8bd2c Merge branch 'develop' into snipe-it-17073-asset-requests-are-not-deleted-when-asset-is-deleted 2025-08-04 12:43:03 -07:00
snipe
c3442033da Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-08-04 18:49:07 +01:00
snipe
f1dd84edba Added option to clone original images
Signed-off-by: snipe <snipe@snipe.net>
2025-08-04 18:47:26 +01:00
snipe
06b040a337 Nicer padding
Signed-off-by: snipe <snipe@snipe.net>
2025-08-02 18:41:26 +01:00
snipe
fa546ddc5b Merge pull request #17510 from grokability/fixes-#17498-add-serial-to-acceptance
Fixed #17498 - added serial to user acceptance
2025-08-02 14:46:46 +01:00
snipe
f811352c79 Cleaned up HTML
Signed-off-by: snipe <snipe@snipe.net>
2025-08-02 14:46:34 +01:00
snipe
7ed8963b9f Fixed #17498 - added serial to user acceptance
Signed-off-by: snipe <snipe@snipe.net>
2025-08-02 14:38:57 +01:00
snipe
a9fc8b79fd Merge pull request #17508 from grokability/add-table-buttons
Add table buttons and admin filter
2025-08-01 23:12:04 +01:00
snipe
afd794b4c7 Fixed HTML
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 22:20:17 +01:00
snipe
c4a28f0ec4 Use consistent icon for adding people
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 22:18:02 +01:00
snipe
db343bf795 Tweaked bootstrap admin indicators
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 22:15:13 +01:00
snipe
0157043dc5 Added table buttons to user view
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 21:58:48 +01:00
snipe
a947f9bd32 Fixed delete modal
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 21:30:18 +01:00
snipe
2a4181c7c3 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 21:19:18 +01:00
snipe
30192f5b14 Removed extra modal code
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 21:11:54 +01:00
snipe
c41b5e8844 Fixed license delete check
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 21:11:40 +01:00
snipe
b27928807b Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 20:44:44 +01:00
snipe
16f1b5e23e Added a few more buttons
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 19:31:25 +01:00
snipe
ed651b6869 Use translations
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:55:15 +01:00
snipe
b9d925c7aa Carry admin/superadmin into the API request
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:49:58 +01:00
snipe
3650a29381 Added superadmin/admin formatter
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:49:37 +01:00
snipe
de84ee3693 Cleaned up asset view table
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:47:51 +01:00
snipe
42ba31591d New formatter for icon
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:46:20 +01:00
snipe
a78a243e20 Added admin/superadmin filter to API
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:46:10 +01:00
snipe
38924ced4a Provide the role so we can use it in the javascript
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:45:23 +01:00
snipe
5e8cc66f5c Added scope for admins and superadmins
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 18:45:07 +01:00
snipe
1353837584 More buttons
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 15:58:15 +01:00
snipe
7cb5a89523 Added access keys
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 15:58:07 +01:00
snipe
1db09a7953 Allow category_id in license export by category
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 15:21:12 +01:00
snipe
bc6aa12dd0 Added buttons to table
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 15:20:55 +01:00
snipe
c3bea88979 Added table button JS
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 15:20:39 +01:00
snipe
6e85e466b0 Merge pull request #17493 from grokability/gallery-view-for-file-uploads
Use the file uploads API for file listing tables, adds gallery view for file uploads
2025-08-01 13:27:27 +01:00
snipe
3327cc70c9 Revert pageSize
Signed-off-by: snipe <snipe@snipe.net>
2025-08-01 12:01:57 +01:00
snipe
c9eac66a93 Tweaked button layout
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:21:58 +01:00
snipe
53e9bd6e48 Use updated formatter
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:21:49 +01:00
snipe
eaa18e1efb Use existing actionlog methods instead of inline
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:21:40 +01:00
snipe
afa3dacc31 Check if it’s an accepted/declined file
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:21:22 +01:00
snipe
c803c4a57a Use new formatters
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:20:35 +01:00
snipe
2d3a53e449 Made existing formatters more flexible, removed unused
Signed-off-by: snipe <snipe@snipe.net>
2025-07-31 13:20:24 +01:00
snipe
5e076754ce Merge pull request #17501 from uberbrady/fix_manufacturer_seeder_button
Fixed #17500 [FD-50045] - Make Manufacturer Seeder button work
2025-07-31 04:09:38 +01:00
Brady Wetherington
927e217961 Fix Manufacturer Seeder button 2025-07-30 09:04:04 -06:00
snipe
80b48101aa Added formatter back
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:19:10 +01:00
snipe
08530e6133 Added icon data-dash to formatters
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:11:35 +01:00
snipe
97130ef6c1 Updated IDs to be less generic
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:11:12 +01:00
snipe
da37feae6d Removed comment
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:10:42 +01:00
snipe
f96172e61f Updated manifest
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:10:33 +01:00
snipe
e35477b8db Made modal control more flexible
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:10:26 +01:00
snipe
cea5560a67 Removed duplicated code for modal handling
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 15:07:35 +01:00
snipe
311bd5e67e Use placeholder for delete button
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 03:31:39 +01:00
snipe
1cfddf2a4c Restore old limit code
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 03:31:29 +01:00
snipe
abe58117fe Moved code closer to actions
Signed-off-by: snipe <snipe@snipe.net>
2025-07-30 03:31:01 +01:00
snipe
ee5f89f70d Fixed pagination
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 22:58:00 +01:00
snipe
4f545ed101 Layout tweaks to template
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 22:57:49 +01:00
snipe
136de4208e Added string
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 21:44:45 +01:00
snipe
7650a2c2a7 Sort by created_by desc by default
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 21:44:23 +01:00
snipe
c3d1987fac Switch to panel
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 21:44:06 +01:00
snipe
12ef78bb1c Added PDF embed
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 21:43:58 +01:00
snipe
16c4241a6e WHY does this work? It’s not in the docs
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 05:42:35 +01:00
snipe
4992c77818 Updated template
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 05:41:43 +01:00
snipe
3a0b1de136 Changed table name
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 05:41:36 +01:00
snipe
1c3ef02c7b FIX THIS!!!
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 05:41:15 +01:00
snipe
f268fe9e80 Added gallery card
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 02:03:12 +01:00
snipe
2ed98c17d4 Added print icon
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 02:03:02 +01:00
snipe
571ae4fbfd Use CSS for nowrap
Signed-off-by: snipe <snipe@snipe.net>
2025-07-29 01:20:20 +01:00
Nicky West
c94a8c42f4 Changed NotesController::getList() to NotesController::index() & reordered methods for consistency 2025-07-28 16:57:46 -07:00
Nicky West
16fdb16a56 Changed over to route model binding and simplified logic & gates 2025-07-28 16:55:11 -07:00
Nicky West
822f9a6f28 Fixed deviations from code standards 2025-07-28 16:37:08 -07:00
Nicky West
b264bbf69f feat(api): Add API endpoints for managing asset history notes
- Add POST endpoint to create a history note attached to an asset
- Add GET endpoint to retrieve history notes for an asset
- Add ActionLog factory state for manual notes
- Implement controller methods with authorization checks
- Add feature tests for note creation, retrieval, and access control
- Register new API routes for these endpoints

Supports automation by enabling programmatic asset history note management.
2025-07-28 15:55:37 -07:00
snipe
6e61e94e02 New manifest
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:36:31 +01:00
snipe
6a7972c5a1 Added new formatters
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:36:18 +01:00
snipe
db4fbe315a Added helper to get media type so we know what kind of lightbox to give it
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:36:11 +01:00
snipe
f3613d7103 Fixed typo
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:35:45 +01:00
snipe
cbbed36428 Added multi-file upload for users (bug)
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:35:35 +01:00
snipe
e86e9697b3 Use plural for item type
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:33:25 +01:00
snipe
fd6b2d5715 Simpler blade component calls
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:33:08 +01:00
snipe
fbb36d1665 Fixed file routes
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:32:45 +01:00
snipe
07be1b8192 Added sorting, updated formatters
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:32:25 +01:00
snipe
33880393ac Added string
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:32:00 +01:00
snipe
5123fe7838 Use server side endpoint for filetable blade component
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:31:51 +01:00
snipe
cbe26a365d Made route signature more consistent
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:31:14 +01:00
snipe
f1bb72b2a6 Added custom view extension
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 23:30:51 +01:00
snipe
2c33654395 Fixed #17485 - nicer alert menu if no items are below qty
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 17:50:26 +01:00
snipe
dd86de017e Dev assets one more time just for good luck
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 17:38:27 +01:00
snipe
3eabde9630 Dev assets
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 17:36:22 +01:00
snipe
640c51af31 Merge pull request #17487 from uberbrady/improve_javascript_3
Optimize javascript for smaller files and faster builds (Rebase of #15175)
2025-07-28 17:34:59 +01:00
Brady Wetherington
7167b17d25 Rebased and brought up to current from the original 2025-07-28 09:57:20 -06:00
snipe
8a35948678 Import DB facade
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 16:17:11 +01:00
snipe
0fe63d3fb9 Re-added jquery-ui
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 14:03:12 +01:00
snipe
e4302c3e88 Fixed comment
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 09:13:32 +01:00
snipe
a7df6fb465 Added DB_SOCKET to example env
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 09:11:00 +01:00
snipe
133e7598e0 Merge pull request #17478 from grokability/library-upgrades
Library upgrades
2025-07-28 09:00:02 +01:00
snipe
c1a52ffa75 Bumped jspdf-autotable from ^3.8.4 to ^5.0.2
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:52:52 +01:00
snipe
4f46313388 Bumped tableexport to ^1.33.0
https://www.npmjs.com/package/tableexport.jquery.plugin
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:50:47 +01:00
snipe
03b2cc9cd2 Dev assets
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:43:22 +01:00
snipe
1a2bf8dc95 Bumped boostrap table from 1.24.1 to 1.24.2
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:43:17 +01:00
snipe
dd63fbeb84 Moved webpack to dev dependencies
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:38:26 +01:00
snipe
59e435c418 Bumped additional libraries
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:36:13 +01:00
snipe
f89f0a19b5 Updated axios
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:33:07 +01:00
snipe
cbc6ef95cb Removed babel-preset
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:26:52 +01:00
snipe
0ceecc9e1d Removed jquery UI
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:24:16 +01:00
snipe
c816902025 Updated postcss
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:19:56 +01:00
snipe
cfb03cdca0 Updated imagemin JS
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:07:34 +01:00
snipe
266f77b08c Update svg-sanitize
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 08:02:54 +01:00
snipe
257d58c236 Moved privacy policy link in settings
Signed-off-by: snipe <snipe@snipe.net>
2025-07-28 03:30:10 +01:00
snipe
015f3d936c Merge pull request #17459 from grokability/#17441-add-status-to-id
Fixed #17441 - hardware listings "remembered" page numbers between statuses
2025-07-24 15:54:33 +01:00
snipe
18d2a0ffd7 Fixed #17441 - added status to table IDs
Signed-off-by: snipe <snipe@snipe.net>
2025-07-24 15:47:26 +01:00
snipe
24afde0e46 Updated hash and minor version
Signed-off-by: snipe <snipe@snipe.net>
2025-07-24 15:35:33 +01:00
snipe
8499faa55a Fixed #17458 - use item_id instead of target_id for user history
Signed-off-by: snipe <snipe@snipe.net>
2025-07-24 15:29:36 +01:00
snipe
c60dd809b8 Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-24 13:06:57 +01:00
snipe
297b8e33f2 Merge pull request #17436 from Godmartinz/fix-acceptance-markdown
Fixed #17394 - Changes the acceptance letter salutation to target
2025-07-23 22:55:05 +01:00
spencerrlongg
d0593c6b8d remove some commented things 2025-07-23 16:19:32 -05:00
spencerrlongg
8a40d7e35c tests added, regex validation working 2025-07-23 16:12:19 -05:00
Godfrey M
b670b2014c accidentally removed a line 2025-07-23 09:56:19 -07:00
Godfrey M
440e969f52 remove unnecessary spacing 2025-07-23 09:47:03 -07:00
snipe
14b79f2f1c Fixed typo in id name
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 17:00:09 +01:00
snipe
00cf49a61f Bumped version
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 16:10:47 +01:00
snipe
4f534e0e84 Bumped version/hash
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 16:02:51 +01:00
snipe
83a19fbbbf Merge pull request #17454 from uberbrady/de_flake_action_log_tests
Enforce order by ID for actionlog tests
2025-07-23 15:06:09 +01:00
snipe
610cb884fc Merge pull request #17452 from uberbrady/de_flake_tls_cert_file_test
This test was flaky, probably due to the PHP statcache.
2025-07-23 15:00:59 +01:00
snipe
ba92cec62b Merge pull request #17453 from grokability/#17316-checkbox-format-on-checkin-checkout
Fixed #17316 - handle checkboxes correctly in checkin/checkout
2025-07-23 14:56:24 +01:00
Brady Wetherington
d92e961a52 enforce order by ID for actionlog tests 2025-07-23 14:55:42 +01:00
snipe
b13e74756a Fixed #17316 - handle checkboxes correctly in checkin/checkout
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 14:51:34 +01:00
Brady Wetherington
4ef3072766 This test was flaky, probably due to the PHP statcache. 2025-07-23 14:15:52 +01:00
snipe
e96e2461d3 Merge pull request #17450 from grokability/copy-decrypted-custom-fields-to-clipboard
Fixed #17447 - decrypt before copying to clipboard
2025-07-23 12:41:02 +01:00
snipe
7a2e2be169 Fixed #17447 - decrypt before copying to clipboard
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 12:39:54 +01:00
snipe
8d2a5a7e4a Added location and defaultLoc to searchable relations in audit log
Signed-off-by: snipe <snipe@snipe.net>
2025-07-23 12:28:23 +01:00
snipe
b7b0e4fab5 Merge pull request #17447 from Godmartinz/make-custom-fields-copyable
Adds #17133 Copy ability to all Custom fields
2025-07-23 12:11:47 +01:00
Godfrey M
a624a79b30 add terenary 2025-07-22 16:36:19 -07:00
Godfrey M
313135da6f Merge branch 'develop' into make-custom-fields-copyable 2025-07-22 16:26:57 -07:00
Godfrey M
58d27d1247 move copy button to front 2025-07-22 16:17:52 -07:00
snipe
edfb28168f Merge pull request #17446 from marcusmoore/snipe-it-17445-move-jobtitle-under-assigned_to-in-assettransformer
Fixed #17445 - move jobtitle under assigned_to in AssetTransformer
2025-07-22 20:27:01 +01:00
Godfrey M
8d0e03bb06 fix copy target 2025-07-22 11:57:46 -07:00
Marcus Moore
855f6f77cf Re-add sorting 2025-07-22 11:49:32 -07:00
Godfrey M
6236cffe14 adds copy links for filled custom fields 2025-07-22 11:49:11 -07:00
Marcus Moore
322a71fbb8 Add jobtitleFormatter 2025-07-22 11:37:34 -07:00
Marcus Moore
4d9f8476f3 Update field key in AssetPresenter 2025-07-22 11:07:58 -07:00
Marcus Moore
d7d93b14b2 Move jobtitle under assigned_to 2025-07-22 11:02:26 -07:00
snipe
d1af3ece6e One more tweak to login checkbox
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 15:25:09 +01:00
snipe
8153b20984 Check for demo mode on UI for able to login
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 15:18:34 +01:00
snipe
a50f605c29 Merge pull request #17443 from grokability/added-not-allowed-cursor
Adds disabled cursor on uneditable fields in user create/edit
2025-07-22 15:13:14 +01:00
snipe
daf23edd10 Adds disabled cursor on uneditable fields in user create/edit
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 15:10:27 +01:00
snipe
2eaaeb8259 Merge pull request #17423 from grokability/tighter-permissions-on-non-admins
Tighter permissions on non-admins and demo modes
2025-07-22 14:32:50 +01:00
snipe
a02c62d62c Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 14:12:51 +01:00
snipe
e0232a8e84 Renamed gate
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 14:02:18 +01:00
snipe
6ea5693b2f Updated comment, removed log error statement
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 13:59:58 +01:00
snipe
030c2114d1 Merge pull request #17442 from grokability/user-api-eula-fix
Fixed FD-49886 - Optimize user queries
2025-07-22 13:39:36 +01:00
snipe
2cb18e3668 Remove fields from query - eulas was querying actionlogs
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 13:25:41 +01:00
snipe
cd9f8be563 Optimize for when we already have the counts
Signed-off-by: snipe <snipe@snipe.net>
2025-07-22 13:25:16 +01:00
snipe
a02792e9bf Merge pull request #17300 from uberbrady/add_actionlog_tests
Fixed #17071 - Adding various tests of the contents of ActionLogs for lots of events
2025-07-22 10:51:30 +01:00
snipe
41bb422244 Merge pull request #17439 from marcusmoore/component-file-test-fix
Attempt to fix flaky file upload tests pt2
2025-07-21 23:16:03 +01:00
Marcus Moore
54663d3342 Pass order to api in test 2025-07-21 15:10:35 -07:00
snipe
2529f7369f Merge pull request #17438 from grokability/file-upload-tests-fix
Attempt to fix flaky file upload tests
2025-07-21 22:48:38 +01:00
snipe
909c33dccf Fixed order location
Signed-off-by: snipe <snipe@snipe.net>
2025-07-21 22:45:17 +01:00
snipe
1adc9f1aa9 Attempt to fix flaky tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-21 22:18:15 +01:00
spencerrlongg
e9948f0718 fixes booleans, adds note, changes name 2025-07-21 15:34:08 -05:00
Godfrey M
49da9e58fd changed markdown to point to assignedto name 2025-07-21 12:00:00 -07:00
spencerrlongg
2f74a8afe1 mac address rule working 2025-07-21 12:02:45 -05:00
snipe
f3e288d078 Updated language strings
Signed-off-by: snipe <snipe@snipe.net>
2025-07-21 17:46:49 +01:00
snipe
988000952e Fixed RB-3997
Signed-off-by: snipe <snipe@snipe.net>
2025-07-21 13:48:01 +01:00
snipe
6537f3794b Merge pull request #17292 from Godmartinz/fail_with_inputs
FIXED: #17194 Return to bulk edit with errors and inputs
2025-07-21 12:03:52 +01:00
snipe
d31718ba8a Merge pull request #17389 from grokability/use-transformer-for-api-asset-model-response
Use standard model transformer for asset model API response
2025-07-21 11:52:25 +01:00
snipe
9dd4bc5fa8 Merge pull request #17391 from Godmartinz/add-components-notifications
FIXED: #13844 Adds Webhook and Mail Notifications for Components
2025-07-21 11:51:30 +01:00
snipe
df5f1bd522 Merge pull request #17434 from grokability/dependabot/github_actions/develop/codacy/codacy-analysis-cli-action-4.4.7
Bump codacy/codacy-analysis-cli-action from 4.4.5 to 4.4.7
2025-07-21 11:45:04 +01:00
dependabot[bot]
ddffab9169 Bump codacy/codacy-analysis-cli-action from 4.4.5 to 4.4.7
Bumps [codacy/codacy-analysis-cli-action](https://github.com/codacy/codacy-analysis-cli-action) from 4.4.5 to 4.4.7.
- [Release notes](https://github.com/codacy/codacy-analysis-cli-action/releases)
- [Commits](https://github.com/codacy/codacy-analysis-cli-action/compare/v4.4.5...v4.4.7)

---
updated-dependencies:
- dependency-name: codacy/codacy-analysis-cli-action
  dependency-version: 4.4.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 09:26:25 +00:00
snipe
0c34073582 Namespace fix for presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 17:17:04 +01:00
snipe
14674947cb Fixed test namespace
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 17:15:51 +01:00
snipe
51bccdbd66 Merge pull request #17424 from marcusmoore/chore/livewire-ugprade
Bumped livewire to v3.6.4
2025-07-18 17:12:14 +01:00
snipe
f0fbb3cf36 Uncomment permissions test
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:31:31 +01:00
Brady Wetherington
0cc47aacbe Got tests to pass by making them match our current reality, rather than wishes 2025-07-18 16:14:32 +01:00
snipe
fafd592290 Wrap groups and activated into the other canEditAuthFields gate
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:03:43 +01:00
snipe
40e754b8c3 Additional criteria for the canEditAuthFields gate
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:03:22 +01:00
snipe
483301db7a Changed some of the gating logic for demo mode. Sigh.
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:02:59 +01:00
snipe
218606fbd6 Updated view permissions
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:02:41 +01:00
snipe
c601b8e62c Updated test
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 16:02:11 +01:00
snipe
2bd68ec991 Uncommented importer gate
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 13:17:25 +01:00
snipe
66842648ed Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 13:17:10 +01:00
snipe
ce54b9a7b5 Removed duplicate alert
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 13:16:59 +01:00
Brady Wetherington
8a5f6d2a5d Refactor base test into Trait, clean test output for easier comparison 2025-07-18 13:16:35 +01:00
snipe
1d86a5476f Updated language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 12:45:43 +01:00
snipe
ca4d3f6bce Changed gate name, removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-18 12:45:32 +01:00
Godfrey M
2812f2ce92 remove log 2025-07-17 15:04:42 -07:00
Godfrey M
5c623db798 fix redirect 2025-07-17 14:57:00 -07:00
Marcus Moore
edaf005fe1 Bump livewire to v3.6.4 2025-07-17 14:15:10 -07:00
snipe
4f6e407247 More consistent language degarding the demo
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 21:13:13 +01:00
snipe
e30881239c A few more clean ups for demo mode
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 21:08:50 +01:00
snipe
bbde2cc4b2 Use history blade component
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 21:04:11 +01:00
snipe
16d18c79d7 Fixed email editable field
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 21:03:20 +01:00
snipe
a0d2cb8a03 Clearer (if longer) gate name
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:47:20 +01:00
snipe
1bb5dc7e69 Added one more test
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:40:01 +01:00
Brady Wetherington
58759acfe4 Think I hit _all_ of the tests we need to mess with here 2025-07-17 20:15:01 +01:00
snipe
0cd5136052 Added translations
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:12:52 +01:00
snipe
b3c6fe5369 Use both new gates in user edit
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:12:46 +01:00
snipe
599718f84e Use new gates in controllers
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:12:32 +01:00
snipe
d9a5452388 Defined new gates
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:12:10 +01:00
snipe
0fe49e04bf Attempt to use a gate here?
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:09:27 +01:00
snipe
a98d3fb4dc Check for the format of the permissions (string, object, array)
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:09:17 +01:00
Godfrey M
8c670d1832 clean up 2025-07-17 12:08:49 -07:00
snipe
c232f490bc Show user log
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:08:40 +01:00
snipe
c7280953dd Added/updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 20:08:32 +01:00
Godfrey M
8f4c606c64 remove var dumps 2025-07-17 12:04:33 -07:00
Godfrey M
6740afab42 radio buttons values return correctly 2025-07-17 11:59:09 -07:00
Godfrey M
5df22b3e6a checkboxes properly check 2025-07-17 11:56:52 -07:00
snipe
3d9d18a0d5 Fixed weird CSS quirk
Signed-off-by: snipe <snipe@snipe.net>
2025-07-17 19:22:23 +01:00
Marcus Moore
0102599708 Implement tests 2025-07-16 17:20:28 -07:00
Marcus Moore
960edd4adf Improve clarity 2025-07-16 17:11:00 -07:00
Marcus Moore
3547fa723c Delete requests when asset model is deleted 2025-07-16 17:04:14 -07:00
Marcus Moore
7a456185c6 Add explicit state for assets 2025-07-16 16:57:03 -07:00
Marcus Moore
dd79c3f2d6 Scaffold tests 2025-07-16 16:47:28 -07:00
Marcus Moore
35682d11f0 Add command to clean checkout requests 2025-07-16 14:49:45 -07:00
Marcus Moore
d04b3f0907 Enable test 2025-07-16 13:15:06 -07:00
Marcus Moore
c926358e04 Delete requests when user is deleted 2025-07-16 13:11:59 -07:00
Marcus Moore
856ba52f36 Delete requests when asset is deleted 2025-07-16 12:43:56 -07:00
Marcus Moore
a5bea31154 Scaffold tests 2025-07-16 12:38:08 -07:00
Marcus Moore
2afcc1e384 Add basic tests around asset request index 2025-07-16 12:25:37 -07:00
Godfrey M
fc469707a3 clean up 2025-07-16 10:51:33 -07:00
snipe
77fdc370c7 Merge pull request #17415 from uberbrady/clean_unaccepted_assets_report
[FD-47386, FD-49095] New Artisan command to clean checkout acceptances
2025-07-16 17:34:49 +01:00
snipe
301290fb6d Send emails on acceptance even if signature is not required
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 17:02:04 +01:00
snipe
07fffe2f79 Merge pull request #17410 from grokability/remove-password-from-welcome
Remove password from welcome email, prompt for reset instead
2025-07-16 16:54:07 +01:00
snipe
0227a63fa5 Slightly clearer language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:31:45 +01:00
snipe
27764b863c Updated language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:25:36 +01:00
snipe
032fd75f9e Added default invite password token timeout
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:23:51 +01:00
snipe
0bf4f861f3 Nicer debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:23:25 +01:00
snipe
fd8f90cb52 Added new password broker for longer toekn lifetime
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:23:11 +01:00
snipe
b6c6b025c8 Added expiry language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:20:26 +01:00
snipe
3d89e98d1f Small tweaks to welcome email blade
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:20:15 +01:00
Brady Wetherington
7c5110ed5d Add more action logs tests everywhere I can think of it. 2025-07-16 16:20:06 +01:00
Brady Wetherington
0a474f48ad WIP: Adding various tests of the contents of ActionLogs for lots of events 2025-07-16 16:20:06 +01:00
Brady Wetherington
c409bfd5be New Artisan command to clean checkout acceptances and a migration that runs it 2025-07-16 16:06:23 +01:00
snipe
39d5d5b2e0 Merge branch 'develop' into remove-password-from-welcome 2025-07-16 15:05:13 +01:00
snipe
8a80d9009d Refomatted hidden array
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 12:24:48 +01:00
Godfrey M
f62b5df566 use ternaries instead of optionals 2025-07-15 15:40:21 -07:00
spencerrlongg
826521f053 added rules, still needs a little more... 2025-07-15 15:21:10 -05:00
spencerrlongg
f9b05bc8de more encryption rules extenting laravel's own 2025-07-15 15:03:51 -05:00
spencerrlongg
b8239e8ed9 use laravel validation methods, email works 2025-07-15 14:17:49 -05:00
Godfrey M
214757ab0b fix mailable 2025-07-15 12:04:36 -07:00
Godfrey M
f130186b37 add Component Checkin Mail 2025-07-15 11:56:34 -07:00
Godfrey M
2244eebc3b add Component Checkout Mail 2025-07-15 11:00:39 -07:00
snipe
4176792f2d Translate field
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 16:32:17 +01:00
snipe
1e6cef52c9 Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 15:17:08 +01:00
snipe
a0f4f30a50 Added try/catch
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 15:13:33 +01:00
snipe
4cbf6ac393 Re-add /setup crential email
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:20:13 +01:00
snipe
af7425d8e6 Remove unused variable
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:19:12 +01:00
snipe
3fea909d3f Removed send credentials option from user controller
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:14:10 +01:00
snipe
7c37d40677 Use plaintext in the database so that the password will never be valid
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:13:50 +01:00
snipe
3a97c27350 Removed logging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:13:29 +01:00
snipe
e0516a52a8 Formatting change
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:55 +01:00
snipe
a85ec6efb3 Set token in welcome email constructor
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:42 +01:00
snipe
3795c74814 Added string
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:26 +01:00
snipe
27954dc6d3 Use password reset token
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:18 +01:00
snipe
68c4187a09 Removed email creds option from user create
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:11:15 +01:00
snipe
b9834231f3 Remove email credentials chexkbox
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:08:36 +01:00
snipe
2be343ea1c More specific no password
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 13:11:45 +01:00
snipe
109fe1b62c Use no password as temp password
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 13:11:18 +01:00
snipe
63d691a63c Removed noisy log
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 13:10:48 +01:00
snipe
6f57d6b876 Merge pull request #17407 from grokability/fixes-signature-pad-chrome
Fixed display of acceptance button if signature is not required
2025-07-15 10:58:34 +01:00
snipe
e0bad99ea1 Fixes display of acceptannce button if signature is not required
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 10:55:30 +01:00
snipe
e39eb09cfb Merge pull request #17390 from Godmartinz/unhandled-redirect-error
FIXED redirect option being NULL
2025-07-10 19:41:40 +01:00
Godfrey M
64d397c3f3 add component notification tests 2025-07-10 11:26:10 -07:00
Godfrey M
465ac1d1e1 remove ternary 2025-07-10 08:39:13 -07:00
Godfrey M
18d6becebc populate other_redirect in store method 2025-07-10 08:36:15 -07:00
Godfrey M
3bbd0fdbcd google notifications fires properly 2025-07-09 17:02:51 -07:00
Godfrey M
8214b11da5 MS teams fires properly 2025-07-09 11:44:53 -07:00
Godfrey M
36090bf83e checked in notification fires, updated icon translation usage 2025-07-09 11:35:24 -07:00
Godfrey M
bffb2fe82f checkout notification fires 2025-07-09 11:23:27 -07:00
Godfrey M
500cbf5d92 add component checkout notification, update checkout blade, update listener 2025-07-09 11:12:18 -07:00
Godfrey M
c8b213c190 remove some changes, move error bag 2025-06-24 13:10:32 -07:00
Godfrey M
942de9dce5 got validation to redirect back to form and display 2025-06-24 12:42:07 -07:00
Marcus Moore
69b9b0bbc0 Allow setting id within location-select 2025-06-02 15:53:25 -07:00
Marcus Moore
3c1088f030 Improve variable name 2025-06-02 15:49:16 -07:00
1918 changed files with 51947 additions and 305988 deletions

View File

@@ -4189,6 +4189,51 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "mckaygerhard",
"name": "Герхард PICCORO Lenz McKAY ",
"avatar_url": "https://avatars.githubusercontent.com/u/1571724?v=4",
"profile": "https://github-readme-stats.vercel.app/api?username=mckaygerhard",
"contributions": [
"code"
]
},
{
"login": "FlorestanII",
"name": "Johannes Pollitt",
"avatar_url": "https://avatars.githubusercontent.com/u/15015119?v=4",
"profile": "https://github.com/FlorestanII",
"contributions": [
"code"
]
},
{
"login": "strobelm",
"name": "Michael Strobel",
"avatar_url": "https://avatars.githubusercontent.com/u/14185442?v=4",
"profile": "https://strobelm.de",
"contributions": [
"code"
]
},
{
"login": "nickwest",
"name": "Nicky West",
"avatar_url": "https://avatars.githubusercontent.com/u/634790?v=4",
"profile": "http://nickwest.me",
"contributions": [
"code"
]
},
{
"login": "akaspeh1",
"name": "akaspeh1",
"avatar_url": "https://avatars.githubusercontent.com/u/1347327?v=4",
"profile": "https://github.com/akaspeh1",
"contributions": [
"code"
]
} }
] ]
} }

View File

@@ -28,6 +28,7 @@ PUBLIC_FILESYSTEM_DISK=local_public
# -------------------------------------------- # --------------------------------------------
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=db DB_HOST=db
DB_SOCKET=null
DB_PORT='3306' DB_PORT='3306'
DB_DATABASE=snipeit DB_DATABASE=snipeit
DB_USERNAME=snipeit DB_USERNAME=snipeit
@@ -168,6 +169,7 @@ AWS_DEFAULT_REGION=null
LOGIN_MAX_ATTEMPTS=5 LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60 LOGIN_LOCKOUT_DURATION=60
RESET_PASSWORD_LINK_EXPIRES=900 RESET_PASSWORD_LINK_EXPIRES=900
INVITE_PASSWORD_LINK_EXPIRES=1500
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: MISC # OPTIONAL: MISC

View File

@@ -24,6 +24,7 @@ PUBLIC_FILESYSTEM_DISK=local_public
# -------------------------------------------- # --------------------------------------------
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_SOCKET=null
DB_PORT=3306 DB_PORT=3306
DB_DATABASE=null DB_DATABASE=null
DB_USERNAME=null DB_USERNAME=null
@@ -174,6 +175,7 @@ LOGIN_AUTOCOMPLETE=false
RESET_PASSWORD_LINK_EXPIRES=15 RESET_PASSWORD_LINK_EXPIRES=15
PASSWORD_CONFIRM_TIMEOUT=10800 PASSWORD_CONFIRM_TIMEOUT=10800
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50 PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
INVITE_PASSWORD_LINK_EXPIRES=1500
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: MISC # OPTIONAL: MISC
@@ -191,11 +193,17 @@ LDAP_TIME_LIM=600
IMPORT_TIME_LIMIT=600 IMPORT_TIME_LIMIT=600
IMPORT_MEMORY_LIMIT=500M IMPORT_MEMORY_LIMIT=500M
REPORT_TIME_LIMIT=12000 REPORT_TIME_LIMIT=12000
REQUIRE_SAML=false
API_THROTTLE_PER_MINUTE=120 API_THROTTLE_PER_MINUTE=120
CSV_ESCAPE_FORMULAS=true CSV_ESCAPE_FORMULAS=true
LIVEWIRE_URL_PREFIX=null LIVEWIRE_URL_PREFIX=null
# --------------------------------------------
# OPTIONAL: SAML SETTINGS
# --------------------------------------------
REQUIRE_SAML=false
SAML_KEY_SIZE=2048
# -------------------------------------------- # --------------------------------------------
# OPTIONAL: HASHING # OPTIONAL: HASHING
# -------------------------------------------- # --------------------------------------------

View File

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

View File

@@ -32,11 +32,11 @@ jobs:
steps: steps:
# Checkout the repository to the GitHub Actions runner # Checkout the repository to the GitHub Actions runner
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI - name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@v4.4.5 uses: codacy/codacy-analysis-cli-action@v4.4.7
with: with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations # You can also omit the token and run the tools that support default configurations

View File

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

View File

@@ -42,7 +42,7 @@ jobs:
steps: steps:
# https://github.com/actions/checkout # https://github.com/actions/checkout
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v4 uses: actions/checkout@v5
# https://github.com/docker/setup-buildx-action # https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx - name: Setup Docker Buildx

View File

@@ -42,7 +42,7 @@ jobs:
steps: steps:
# https://github.com/actions/checkout # https://github.com/actions/checkout
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v4 uses: actions/checkout@v5
# https://github.com/docker/setup-buildx-action # https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx - name: Setup Docker Buildx

View File

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

View File

@@ -37,7 +37,7 @@ jobs:
php-version: "${{ matrix.php-version }}" php-version: "${{ matrix.php-version }}"
coverage: none coverage: none
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Get Composer Cache Directory - name: Get Composer Cache Directory
id: composer-cache id: composer-cache

View File

@@ -34,7 +34,7 @@ jobs:
php-version: "${{ matrix.php-version }}" php-version: "${{ matrix.php-version }}"
coverage: none coverage: none
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Get Composer Cache Directory - name: Get Composer Cache Directory
id: composer-cache id: composer-cache

View File

@@ -25,7 +25,7 @@ jobs:
php-version: "${{ matrix.php-version }}" php-version: "${{ matrix.php-version }}"
coverage: none coverage: none
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Get Composer Cache Directory - name: Get Composer Cache Directory
id: composer-cache id: composer-cache

View File

@@ -68,6 +68,7 @@ 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/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/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/6865786?v=4" width="110px;"/><br /><sub>Jan Felix Wiebe</sub>](https://github.com/jfwiebe)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jfwiebe "Code") | [<img src="https://avatars.githubusercontent.com/u/43412008?v=4" width="110px;"/><br /><sub>Jo Drexl</sub>](https://www.nfon.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=drexljo "Code") | [<img src="https://avatars.githubusercontent.com/u/4807843?v=4" width="110px;"/><br /><sub>Austin Sasko</sub>](https://github.com/austinsasko)<br />[💻](https://github.com/snipe/snipe-it/commits?author=austinsasko "Code") | [<img src="https://avatars.githubusercontent.com/u/4875039?v=4" width="110px;"/><br /><sub>Jasson</sub>](http://jassoncordones.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JassonCordones "Code") | [<img src="https://avatars.githubusercontent.com/u/76069640?v=4" width="110px;"/><br /><sub>Okean</sub>](https://github.com/Tinyblargon)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tinyblargon "Code") | [<img src="https://avatars.githubusercontent.com/u/6515064?v=4" width="110px;"/><br /><sub>Alejandro Medrano</sub>](https://www.lst.tfo.upm.es/alejandro-medrano/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=amedranogil "Code") | [<img src="https://avatars.githubusercontent.com/u/58696401?v=4" width="110px;"/><br /><sub>Lukas Kraic</sub>](https://github.com/lukaskraic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukaskraic "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1571724?v=4" width="110px;"/><br /><sub>Герхард PICCORO Lenz McKAY </sub>](https://github-readme-stats.vercel.app/api?username=mckaygerhard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mckaygerhard "Code") | [<img src="https://avatars.githubusercontent.com/u/15015119?v=4" width="110px;"/><br /><sub>Johannes Pollitt</sub>](https://github.com/FlorestanII)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorestanII "Code") | [<img src="https://avatars.githubusercontent.com/u/14185442?v=4" width="110px;"/><br /><sub>Michael Strobel</sub>](https://strobelm.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=strobelm "Code") | [<img src="https://avatars.githubusercontent.com/u/634790?v=4" width="110px;"/><br /><sub>Nicky West</sub>](http://nickwest.me)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nickwest "Code") | [<img src="https://avatars.githubusercontent.com/u/1347327?v=4" width="110px;"/><br /><sub>akaspeh1</sub>](https://github.com/akaspeh1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akaspeh1 "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END --> <!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View File

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

View File

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

View File

@@ -55,6 +55,8 @@ class LdapSync extends Command
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M')); ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
// Map the LDAP attributes to the Snipe-IT user fields.
$ldap_map = [ $ldap_map = [
"username" => Setting::getSettings()->ldap_username_field, "username" => Setting::getSettings()->ldap_username_field,
"last_name" => Setting::getSettings()->ldap_lname_field, "last_name" => Setting::getSettings()->ldap_lname_field,
@@ -63,11 +65,17 @@ class LdapSync extends Command
"emp_num" => Setting::getSettings()->ldap_emp_num, "emp_num" => Setting::getSettings()->ldap_emp_num,
"email" => Setting::getSettings()->ldap_email, "email" => Setting::getSettings()->ldap_email,
"phone" => Setting::getSettings()->ldap_phone_field, "phone" => Setting::getSettings()->ldap_phone_field,
"mobile" => Setting::getSettings()->ldap_mobile,
"jobtitle" => Setting::getSettings()->ldap_jobtitle, "jobtitle" => Setting::getSettings()->ldap_jobtitle,
"address" => Setting::getSettings()->ldap_address,
"city" => Setting::getSettings()->ldap_city,
"state" => Setting::getSettings()->ldap_state,
"zip" => Setting::getSettings()->ldap_zip,
"country" => Setting::getSettings()->ldap_country, "country" => Setting::getSettings()->ldap_country,
"location" => Setting::getSettings()->ldap_location, "location" => Setting::getSettings()->ldap_location,
"dept" => Setting::getSettings()->ldap_dept, "dept" => Setting::getSettings()->ldap_dept,
"manager" => Setting::getSettings()->ldap_manager, "manager" => Setting::getSettings()->ldap_manager,
"display_name" => Setting::getSettings()->ldap_display_name,
]; ];
$ldap_default_group = Setting::getSettings()->ldap_default_group; $ldap_default_group = Setting::getSettings()->ldap_default_group;
@@ -234,9 +242,11 @@ class LdapSync extends Command
} }
// Assign the mapped LDAP attributes for each user to the Snipe-IT user fields
for ($i = 0; $i < $results['count']; $i++) { for ($i = 0; $i < $results['count']; $i++) {
$item = []; $item = [];
$item['username'] = $results[$i][$ldap_map["username"]][0] ?? ''; $item['username'] = $results[$i][$ldap_map["username"]][0] ?? '';
$item['display_name'] = $results[$i][$ldap_map["display_name"]][0] ?? '';
$item['employee_number'] = $results[$i][$ldap_map["emp_num"]][0] ?? ''; $item['employee_number'] = $results[$i][$ldap_map["emp_num"]][0] ?? '';
$item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? ''; $item['lastname'] = $results[$i][$ldap_map["last_name"]][0] ?? '';
$item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? ''; $item['firstname'] = $results[$i][$ldap_map["first_name"]][0] ?? '';
@@ -244,8 +254,13 @@ class LdapSync extends Command
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? ''; $item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
$item['location_id'] = $results[$i]['location_id'] ?? ''; $item['location_id'] = $results[$i]['location_id'] ?? '';
$item['telephone'] = $results[$i][$ldap_map["phone"]][0] ?? ''; $item['telephone'] = $results[$i][$ldap_map["phone"]][0] ?? '';
$item['mobile'] = $results[$i][$ldap_map["mobile"]][0] ?? '';
$item['jobtitle'] = $results[$i][$ldap_map["jobtitle"]][0] ?? ''; $item['jobtitle'] = $results[$i][$ldap_map["jobtitle"]][0] ?? '';
$item['address'] = $results[$i][$ldap_map["address"]][0] ?? '';
$item['city'] = $results[$i][$ldap_map["city"]][0] ?? '';
$item['state'] = $results[$i][$ldap_map["state"]][0] ?? '';
$item['country'] = $results[$i][$ldap_map["country"]][0] ?? ''; $item['country'] = $results[$i][$ldap_map["country"]][0] ?? '';
$item['zip'] = $results[$i][$ldap_map["zip"]][0] ?? '';
$item['department'] = $results[$i][$ldap_map["dept"]][0] ?? ''; $item['department'] = $results[$i][$ldap_map["dept"]][0] ?? '';
$item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? ''; $item['manager'] = $results[$i][$ldap_map["manager"]][0] ?? '';
$item['location'] = $results[$i][$ldap_map["location"]][0] ?? ''; $item['location'] = $results[$i][$ldap_map["location"]][0] ?? '';
@@ -278,6 +293,9 @@ class LdapSync extends Command
if($ldap_map["username"] != null){ if($ldap_map["username"] != null){
$user->username = $item['username']; $user->username = $item['username'];
} }
if($ldap_map["display_name"] != null){
$user->display_name = $item['display_name'];
}
if($ldap_map["last_name"] != null){ if($ldap_map["last_name"] != null){
$user->last_name = $item['lastname']; $user->last_name = $item['lastname'];
} }
@@ -293,6 +311,9 @@ class LdapSync extends Command
if($ldap_map["phone"] != null){ if($ldap_map["phone"] != null){
$user->phone = $item['telephone']; $user->phone = $item['telephone'];
} }
if($ldap_map["mobile"] != null){
$user->mobile = $item['mobile'];
}
if($ldap_map["jobtitle"] != null){ if($ldap_map["jobtitle"] != null){
$user->jobtitle = $item['jobtitle']; $user->jobtitle = $item['jobtitle'];
} }

View File

@@ -6,6 +6,7 @@ use Illuminate\Console\Command;
use App\Models\Setting; use App\Models\Setting;
use Exception; use Exception;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use App\Models\Ldap;
/** /**
* Check if a given ip is in a network * Check if a given ip is in a network
@@ -160,7 +161,15 @@ class LdapTroubleshooter extends Command
$output[] = "-x"; $output[] = "-x";
$output[] = "-b ".escapeshellarg($settings->ldap_basedn); $output[] = "-b ".escapeshellarg($settings->ldap_basedn);
$output[] = "-D ".escapeshellarg($settings->ldap_uname); $output[] = "-D ".escapeshellarg($settings->ldap_uname);
$output[] = "-w ".escapeshellarg(Crypt::Decrypt($settings->ldap_pword));
try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
exit(0);
}
$output[] = "-w ". escapeshellarg($w);
$output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter)); $output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter));
if($settings->ldap_tls) { if($settings->ldap_tls) {
$this->line("# adding STARTTLS option"); $this->line("# adding STARTTLS option");
@@ -171,6 +180,23 @@ class LdapTroubleshooter extends Command
$this->line(implode(" \\\n",$output)); $this->line(implode(" \\\n",$output));
exit(0); exit(0);
} }
//PHP Version check for warning
$php_version = phpversion();
list($major, $minor, $patch) = explode('.', $php_version);
if (
$major < 8 ||
($major == 8 && $minor < 3) ||
($major == 8 && $minor == 3 && $patch < 21) ||
($major == 8 && $minor == 4 && $patch < 7)
) {
$this->warn("PHP Version: $php_version WARNING - Versions before 8.3.21 or 8.4.7 will return INCONSISTENT results!");
if (!$this->confirm("Are you sure you wish to continue?")) {
$this->warn("ABORTING");
exit(-1);
}
}
if(!$this->option('force')) { if(!$this->option('force')) {
$confirmation = $this->confirm('WARNING: This command will make several attempts to connect to your LDAP server. Are you sure this is ok?'); $confirmation = $this->confirm('WARNING: This command will make several attempts to connect to your LDAP server. Are you sure this is ok?');
if(!$confirmation) { if(!$confirmation) {
@@ -179,7 +205,7 @@ class LdapTroubleshooter extends Command
} }
} }
//$this->line(print_r($settings,true)); //$this->line(print_r($settings,true));
$this->info("STAGE 1: Checking settings"); $this->line("STAGE 1: Checking settings");
if(!$settings->ldap_enabled) { if(!$settings->ldap_enabled) {
$this->error("WARNING: Snipe-IT's LDAP setting is not turned on. (That may be OK if you're still trying to figure out settings)"); $this->error("WARNING: Snipe-IT's LDAP setting is not turned on. (That may be OK if you're still trying to figure out settings)");
} }
@@ -210,32 +236,40 @@ class LdapTroubleshooter extends Command
$this->info("Determined LDAP hostname to be: ".$parsed['host']); $this->info("Determined LDAP hostname to be: ".$parsed['host']);
} }
$this->info("Performing DNS lookup of: ".$parsed['host']);
$ips = dns_get_record($parsed['host']);
$raw_ips = []; $raw_ips = [];
//$this->info("Host IP is: ".print_r($ips,true)); if (inet_pton($parsed['host']) !== false) {
$this->line($parsed['host'] . " already looks like an address; skipping DNS lookup");
$raw_ips[] = $parsed['host'];
} else {
$this->line("Performing DNS lookup of: " . $parsed['host']);
$ips = dns_get_record($parsed['host']);
if(!$ips || count($ips) == 0) { //$this->info("Host IP is: ".print_r($ips,true));
$this->error("ERROR: DNS lookup of host: ".$parsed['host']." has failed. ABORTING.");
exit(-1); if (!$ips || count($ips) == 0) {
} $this->error("ERROR: DNS lookup of host: " . $parsed['host'] . " has failed. ABORTING.");
$this->debugout("IP's? ".print_r($ips,true)); exit(-1);
foreach($ips as $ip) {
if(!isset($ip['ip'])) {
continue;
} }
$raw_ips[]=$ip['ip']; $this->debugout("IP's? " . print_r($ips, true));
if($ip['ip'] == "127.0.0.1") { foreach ($ips as $ip) {
if (!isset($ip['ip'])) {
continue;
}
$raw_ips[] = $ip['ip'];
}
}
foreach ($raw_ips as $ip) {
if ($ip == "127.0.0.1") {
$this->error("WARNING: Using the localhost IP as the LDAP server. This is usually wrong"); $this->error("WARNING: Using the localhost IP as the LDAP server. This is usually wrong");
} }
if(ip_in_range($ip['ip'],'10.0.0.0/8') || ip_in_range($ip['ip'],'192.168.0.0/16') || ip_in_range($ip['ip'], '172.16.0.0/12')) { if (ip_in_range($ip, '10.0.0.0/8') || ip_in_range($ip, '192.168.0.0/16') || ip_in_range($ip, '172.16.0.0/12')) {
$this->error("WARNING: Using an RFC1918 Private address for LDAP server. This may be correct, but it can be a problem if your Snipe-IT instance is not hosted on your private network"); $this->error("WARNING: Using an RFC1918 Private address for LDAP server. This may be correct, but it can be a problem if your Snipe-IT instance is not hosted on your private network");
} }
} }
$this->info("STAGE 2: Checking basic network connectivity"); $this->line("STAGE 2: Checking basic network connectivity");
$ports = [389,636]; $ports = [636, 389];
if(@$parsed['port'] && !in_array($parsed['port'],$ports)) { if(@$parsed['port'] && !in_array($parsed['port'],$ports)) {
$ports[] = $parsed['port']; $ports[] = $parsed['port'];
} }
@@ -246,7 +280,7 @@ class LdapTroubleshooter extends Command
$errstr = ''; $errstr = '';
$timeout = 30.0; $timeout = 30.0;
$result = ''; $result = '';
$this->info("Attempting to connect to port: ".$port." - may take up to $timeout seconds"); $this->line("Attempting to connect to port: " . $port . " - may take up to $timeout seconds");
try { try {
$result = fsockopen($parsed['host'], $port, $errno, $errstr, 30.0); $result = fsockopen($parsed['host'], $port, $errno, $errstr, 30.0);
} catch(Exception $e) { } catch(Exception $e) {
@@ -265,9 +299,9 @@ class LdapTroubleshooter extends Command
exit(-1); exit(-1);
} }
$this->info("STAGE 3: Determine encryption algorithm, if any"); $this->line("STAGE 3: Determine encryption algorithm, if any");
$ldap_urls = []; $ldap_urls = []; // [url, cert-check?, start_tls?]
$pretty_ldap_urls = []; $pretty_ldap_urls = [];
foreach($open_ports as $port) { foreach($open_ports as $port) {
$this->line("Trying TLS first for port $port"); $this->line("Trying TLS first for port $port");
@@ -275,35 +309,46 @@ class LdapTroubleshooter extends Command
if($this->test_anonymous_bind($ldap_url)) { if($this->test_anonymous_bind($ldap_url)) {
$this->info("Anonymous bind succesful to $ldap_url!"); $this->info("Anonymous bind succesful to $ldap_url!");
$ldap_urls[] = [ $ldap_url, true, false ]; $ldap_urls[] = [ $ldap_url, true, false ];
$pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ]; $pretty_ldap_urls[] = [$ldap_url, "enabled", "n/a (no)"];
continue; // TODO - lots of copypasta in these if(test_anonymous_bind()) routines... continue; // TODO - lots of copypasta in these if(test_anonymous_bind()) routines...
} else { } else {
$this->error("WARNING: Failed to bind to $ldap_url - trying without certificate checks."); $this->error("WARNING: Failed to bind to $ldap_url - trying without certificate checks.");
} }
if($this->test_anonymous_bind($ldap_url, false)) { if($this->test_anonymous_bind($ldap_url, false)) {
$this->info("Anonymous bind succesful to $ldap_url with certifcate-checks disabled"); $this->info("Anonymous bind successful to $ldap_url with certificate-checks disabled");
$ldap_urls[] = [ $ldap_url, false, false ]; $ldap_urls[] = [$ldap_url, false, false];
$pretty_ldap_urls[] = [ $ldap_url, "no", "no" ]; $pretty_ldap_urls[] = [$ldap_url, "DISABLED", "n/a (no)"];
continue; continue;
} else { } else {
$this->error("WARNING: Failed to bind to $ldap_url with certificate checks disabled. Trying unencrypted with STARTTLS"); $this->error("WARNING: Failed to bind to $ldap_url with certificate checks disabled. Trying unencrypted with STARTTLS");
} }
// now switching to ldap:// URL's from ldaps://
$ldap_url = "ldap://".$parsed['host'].":$port"; $ldap_url = "ldap://".$parsed['host'].":$port";
if($this->test_anonymous_bind($ldap_url, true, true)) { if($this->test_anonymous_bind($ldap_url, true, true)) {
$this->info("Plain connection to $ldap_url with STARTTLS succesful!"); $this->info("Plain connection to $ldap_url with STARTTLS succesful!");
$ldap_urls[] = [ $ldap_url, true, true ]; $ldap_urls[] = [ $ldap_url, true, true ];
$pretty_ldap_urls[] = [ $ldap_url, "YES", "YES" ]; $pretty_ldap_urls[] = [$ldap_url, "enabled", "STARTTLS ENABLED"];
continue; continue;
} else { } else {
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without STARTTLS"); $this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without certificate checks.");
}
if ($this->test_anonymous_bind($ldap_url, false, true)) {
$this->info("Plain connection to $ldap_url with STARTTLS and cert checks *disabled* successful!");
$ldap_urls[] = [$ldap_url, false, true];
$pretty_ldap_urls[] = [$ldap_url, "DISABLED", "STARTTLS ENABLED"];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled, and cert checks disabled. Trying without STARTTLS");
} }
if($this->test_anonymous_bind($ldap_url)) { if($this->test_anonymous_bind($ldap_url)) {
$this->info("Plain connection to $ldap_url succesful!"); $this->info("Plain connection to $ldap_url succesful!");
$ldap_urls[] = [ $ldap_url, true, false ]; $ldap_urls[] = [ $ldap_url, true, false ];
$pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ]; $pretty_ldap_urls[] = [$ldap_url, "n/a", "starttls disabled"];
continue; continue;
} else { } else {
$this->error("WARNING: Failed to bind to $ldap_url. Giving up on port $port"); $this->error("WARNING: Failed to bind to $ldap_url. Giving up on port $port");
@@ -313,23 +358,29 @@ class LdapTroubleshooter extends Command
$this->debugout(print_r($ldap_urls,true)); $this->debugout(print_r($ldap_urls,true));
if(count($ldap_urls) > 0 ) { if(count($ldap_urls) > 0 ) {
$this->info("Found working LDAP URL's: "); $this->debugout("Found working LDAP URL's: ");
foreach($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead? foreach($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead?
$this->info("LDAP URL: ".$ldap_url[0]); $this->debugout("LDAP URL: " . $ldap_url[0]);
$this->info($ldap_url[0]. ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled"). ($ldap_url[2] ? " STARTTLS Enabled ": " STARTTLS Disabled")); $this->debugout($ldap_url[0] . ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled") . ($ldap_url[2] ? " STARTTLS Enabled " : " STARTTLS Disabled"));
} }
$this->table(["URL", "Cert Checks Enabled?", "STARTTLS Enabled?"],$pretty_ldap_urls); $this->table(["URL", "Cert Checks?", "STARTTLS?"], $pretty_ldap_urls);
} else { } else {
$this->error("ERROR - no valid LDAP URL's available - ABORTING"); $this->error("ERROR - no valid LDAP URL's available - ABORTING");
exit(1); exit(1);
} }
$this->info("STAGE 4: Test Administrative Bind for LDAP Sync"); $this->line("STAGE 4: Test Administrative Bind for LDAP Sync");
foreach($ldap_urls AS $ldap_url) { foreach($ldap_urls AS $ldap_url) {
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, Crypt::decrypt($settings->ldap_pword)); try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
exit(0);
}
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, $w);
} }
$this->info("STAGE 5: Test BaseDN"); $this->line("STAGE 5: Test BaseDN");
//grab all LDAP_ constants and fill up a reversed array mapping from weird LDAP dotted-strings to (Constant Name) //grab all LDAP_ constants and fill up a reversed array mapping from weird LDAP dotted-strings to (Constant Name)
$all_defined_constants = get_defined_constants(); $all_defined_constants = get_defined_constants();
$ldap_constants = []; $ldap_constants = [];
@@ -341,16 +392,23 @@ class LdapTroubleshooter extends Command
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true)); $this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
foreach($ldap_urls AS $ldap_url) { foreach($ldap_urls AS $ldap_url) {
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,Crypt::decrypt($settings->ldap_pword),$settings)) { try {
$w = Crypt::Decrypt($settings->ldap_pword);
} catch (\Exception $e) {
$this->warn("Could not decrypt password. This usually means an LDAP password was not set or the APP_KEY was changed since the LDAP pasword was last saved. Aborting.");
exit(0);
}
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,$w,$settings)) {
$this->info("Success getting informational bind!"); $this->info("Success getting informational bind!");
} else { } else {
$this->error("Unable to get information from bind."); $this->error("Unable to get information from bind.");
} }
} }
$this->info("STAGE 6: Test LDAP Login to Snipe-IT"); $this->line("STAGE 6: Test LDAP Login to Snipe-IT");
foreach($ldap_urls AS $ldap_url) { foreach($ldap_urls AS $ldap_url) {
$this->info("Starting auth to ".$ldap_url[0]); $this->line("Starting auth to " . $ldap_url[0]);
while(true) { while(true) {
$with_tls = $ldap_url[1] ? "with": "without"; $with_tls = $ldap_url[1] ? "with": "without";
$with_startssl = $ldap_url[2] ? "using": "not using"; $with_startssl = $ldap_url[2] ? "using": "not using";
@@ -359,7 +417,12 @@ class LdapTroubleshooter extends Command
} }
$username = $this->ask("Username"); $username = $this->ask("Username");
$password = $this->secret("Password"); $password = $this->secret("Password");
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results? $results = $this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results?
if ($results) {
$this->info("Success authenticating with " . $username);
} else {
$this->error("Unable to authenticate with " . $username);
}
} }
} }
@@ -368,14 +431,17 @@ class LdapTroubleshooter extends Command
public function connect_to_ldap($ldap_url, $check_cert, $start_tls) public function connect_to_ldap($ldap_url, $check_cert, $start_tls)
{ {
if ($check_cert) {
$this->line("we *ARE* checking certs");
Ldap::ignoreCertificates(false);
} else {
$this->line("we are IGNORING certs");
Ldap::ignoreCertificates(true);
}
$lconn = ldap_connect($ldap_url); $lconn = ldap_connect($ldap_url);
ldap_set_option($lconn, LDAP_OPT_PROTOCOL_VERSION, 3); // should we 'test' different protocol versions here? Does anyone even use anything other than LDAPv3? ldap_set_option($lconn, LDAP_OPT_PROTOCOL_VERSION, 3); // should we 'test' different protocol versions here? Does anyone even use anything other than LDAPv3?
// no - it's formally deprecated: https://tools.ietf.org/html/rfc3494 // no - it's formally deprecated: https://tools.ietf.org/html/rfc3494
if(!$check_cert) {
putenv('LDAPTLS_REQCERT=never'); // This is horrible; is this *really* the only way to do it?
} else {
putenv('LDAPTLS_REQCERT'); // have to very explicitly and manually *UN* set the env var here to ensure it works
}
if($this->settings->ldap_client_tls_cert && $this->settings->ldap_client_tls_key) { if($this->settings->ldap_client_tls_cert && $this->settings->ldap_client_tls_key) {
// client-side TLS certificate support for LDAP (Google Secure LDAP) // client-side TLS certificate support for LDAP (Google Secure LDAP)
putenv('LDAPTLS_CERT=storage/ldap_client_tls.cert'); putenv('LDAPTLS_CERT=storage/ldap_client_tls.cert');
@@ -404,9 +470,10 @@ class LdapTroubleshooter extends Command
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert , $start_tls) { return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert , $start_tls) {
try { try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls); $lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$this->info("gonna try to bind now, this can take a while if we mess it up"); $this->line("Attempting to bind now, this can take a while if we mess it up");
$bind_results = ldap_bind($lconn); $bind_results = ldap_bind($lconn);
$this->info("Bind results are: ".$bind_results." which translate into boolean: ".(bool)$bind_results); $this->line("Bind results are: " . $bind_results . " which translate into boolean: " . (bool)$bind_results);
ldap_close($lconn);
return (bool)$bind_results; return (bool)$bind_results;
} catch (Exception $e) { } catch (Exception $e) {
$this->error("WARNING: Exception caught during bind - ".$e->getMessage()); $this->error("WARNING: Exception caught during bind - ".$e->getMessage());
@@ -421,6 +488,7 @@ class LdapTroubleshooter extends Command
try { try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls); $lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$bind_results = ldap_bind($lconn, $username, $password); $bind_results = ldap_bind($lconn, $username, $password);
ldap_close($lconn);
if(!$bind_results) { if(!$bind_results) {
$this->error("WARNING: Failed to bind to $ldap_url as $username"); $this->error("WARNING: Failed to bind to $ldap_url as $username");
return false; return false;
@@ -446,22 +514,62 @@ class LdapTroubleshooter extends Command
return false; return false;
} }
$this->info("SUCCESS - Able to bind to $ldap_url as $username"); $this->info("SUCCESS - Able to bind to $ldap_url as $username");
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/); $cleaned_results = [];
$results = ldap_get_entries($conn, $result); try {
$cleaned_results = $this->ldap_results_cleaner($results); // This _may_ only work for Active Directory?
$this->line(print_r($cleaned_results,true)); $result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
//okay, great - now how do we display those results? I have no idea. $results = ldap_get_entries($conn, $result);
$cleaned_results = $this->ldap_results_cleaner($results);
//$this->line(print_r($cleaned_results,true));
$default_naming_contexts = $cleaned_results[0]['namingcontexts'];
$this->info("Default Naming Contexts:");
$this->info(implode(", ", $default_naming_contexts));
//okay, great - now how do we display those results? I have no idea.
} catch (\Exception $e) {
$this->error("Unable to get base naming contexts - here's what we *did* get:");
$this->line(print_r($cleaned_results, true));
}
// I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it? // I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it?
$this->comment("I guess we're trying to do the ldap search here, but sometimes it takes too long?"); $this->debugout("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
$this->debugout("Base DN is: ".$settings->ldap_basedn." and filter is: ".parenthesized_filter($settings->ldap_filter)); $this->debugout("Base DN is: ".$settings->ldap_basedn." and filter is: ".parenthesized_filter($settings->ldap_filter));
$search_results = ldap_search($conn, $settings->ldap_basedn, parenthesized_filter($settings->ldap_filter)); $search_results = ldap_search($conn, $settings->ldap_basedn, parenthesized_filter($settings->ldap_filter));
$entries = ldap_get_entries($conn, $search_results);
$this->info("Printing first 10 results: "); $this->info("Printing first 10 results: ");
for($i=0;$i<10;$i++) { $pretty_data = array_slice($this->ldap_results_cleaner($entries), 0, 10);
$this->info($search_results[$i]); //print_r($data);
$headers = [];
foreach ($pretty_data as $row) {
//populate headers
foreach ($row as $key => $value) {
//skip objectsid and objectguid because it junks up output
if ($key == "objectsid" || $key == "objectguid") {
continue;
}
if (!in_array($key, $headers)) {
$headers[] = $key;
}
}
} }
$table = [];
//repeat again to populate table
foreach ($pretty_data as $row) {
$newrow = [];
foreach ($headers as $header) {
if (is_array(@$row[$header])) {
$newrow[] = "[" . implode(", ", $row[$header]) . "]";
} else {
$newrow[] = @$row[$header];
}
}
$table[] = $newrow;
}
$this->table($headers, $table);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage()); $this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
return false; return false;
} finally {
ldap_close($conn);
} }
}); });
} }
@@ -477,7 +585,7 @@ class LdapTroubleshooter extends Command
{ {
if(!(function_exists('pcntl_sigtimedwait') && function_exists('posix_getpid') && function_exists('pcntl_fork') && function_exists('posix_kill') && function_exists('pcntl_wifsignaled'))) { if(!(function_exists('pcntl_sigtimedwait') && function_exists('posix_getpid') && function_exists('pcntl_fork') && function_exists('posix_kill') && function_exists('pcntl_wifsignaled'))) {
// POSIX functions needed for forking aren't present, just run the function inline (ignoring timeout) // POSIX functions needed for forking aren't present, just run the function inline (ignoring timeout)
$this->info('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected'); $this->line('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
return $function(); return $function();
} else { } else {
$parent_pid = posix_getpid(); $parent_pid = posix_getpid();
@@ -514,4 +622,6 @@ class LdapTroubleshooter extends Command
} }
} }
} }

View File

@@ -96,7 +96,7 @@ class MoveUploadsToNewDisk extends Command
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*"); $private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*"); $private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*"); $private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
$private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'."/*.*"); $private_uploads['assetmodels'] = glob('storage/private_uploads/models'."/*.*");
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*"); $private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*"); $private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*"); $private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");

View File

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

View File

@@ -243,6 +243,8 @@ class RestoreFromBackup extends Command
$private_dirs = [ $private_dirs = [
'storage/private_uploads/accessories', 'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels', 'storage/private_uploads/assetmodels',
'storage/private_uploads/maintenances',
'storage/private_uploads/models',
'storage/private_uploads/assets', // these are asset _files_, not the pictures. 'storage/private_uploads/assets', // these are asset _files_, not the pictures.
'storage/private_uploads/audits', 'storage/private_uploads/audits',
'storage/private_uploads/components', 'storage/private_uploads/components',
@@ -260,9 +262,10 @@ class RestoreFromBackup extends Command
]; ];
$public_dirs = [ $public_dirs = [
'public/uploads/accessories', 'public/uploads/accessories',
'public/uploads/assetmodels',
'public/uploads/maintenances',
'public/uploads/assets', // these are asset _pictures_, not asset files 'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/avatars', 'public/uploads/avatars',
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/categories', 'public/uploads/categories',
'public/uploads/companies', 'public/uploads/companies',
'public/uploads/components', 'public/uploads/components',

View File

@@ -77,7 +77,7 @@ class SendAcceptanceReminder extends Command
if(!$email){ if(!$email){
$no_email_list[] = [ $no_email_list[] = [
'id' => $acceptance->assignedTo?->id, 'id' => $acceptance->assignedTo?->id,
'name' => $acceptance->assignedTo?->present()->fullName(), 'name' => $acceptance->assignedTo?->display_name,
]; ];
} else { } else {
$count++; $count++;

View File

@@ -133,9 +133,18 @@ class Handler extends ExceptionHandler
// This is traaaaash but it handles models that are not found while using route model binding :( // This is traaaaash but it handles models that are not found while using route model binding :(
// The only alternative is to set that at *each* route, which is crazypants // The only alternative is to set that at *each* route, which is crazypants
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) { if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
$ids = method_exists($e, 'getIds') ? $e->getIds() : [];
if (in_array('bulkedit', $ids, true)) {
$error_array = session()->get('bulk_asset_errors');
return redirect()
->route('hardware.index')
->withErrors($error_array, 'bulk_asset_errors')
->withInput();
}
// This gets the MVC model name from the exception and formats in a way that's less fugly // This gets the MVC model name from the exception and formats in a way that's less fugly
$model_name = strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel()))))); $model_name = trim(strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel()))))));
$route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index'; $route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
// Sigh. // Sigh.
@@ -151,9 +160,7 @@ class Handler extends ExceptionHandler
$route = 'maintenances.index'; $route = 'maintenances.index';
} elseif ($route === 'licenseseats.index') { } elseif ($route === 'licenseseats.index') {
$route = 'licenses.index'; $route = 'licenses.index';
} elseif ($route === 'customfields.index') { } elseif (($route === 'customfieldsets.index') || ($route === 'customfields.index')) {
$route = 'fields.index';
} elseif ($route === 'customfieldsets.index') {
$route = 'fields.index'; $route = 'fields.index';
} }

View File

@@ -1197,19 +1197,30 @@ class Helper
'webp' => 'far fa-image', 'webp' => 'far fa-image',
'avif' => 'far fa-image', 'avif' => 'far fa-image',
'svg' => 'fas fa-vector-square', 'svg' => 'fas fa-vector-square',
// word // word
'doc' => 'far fa-file-word', 'doc' => 'far fa-file-word',
'docx' => 'far fa-file-word', 'docx' => 'far fa-file-word',
// Excel // Excel
'xls' => 'far fa-file-excel', 'xls' => 'far fa-file-excel',
'xlsx' => 'far fa-file-excel', 'xlsx' => 'far fa-file-excel',
'ods' => 'far fa-file-excel',
// Presentation
'ppt' => 'far fa-file-powerpoint',
'odp' => 'far fa-file-powerpoint',
// archive // archive
'zip' => 'fas fa-file-archive', 'zip' => 'fas fa-file-archive',
'rar' => 'fas fa-file-archive', 'rar' => 'fas fa-file-archive',
//Text //Text
'odt' => 'far fa-file-alt',
'txt' => 'far fa-file-alt', 'txt' => 'far fa-file-alt',
'rtf' => 'far fa-file-alt', 'rtf' => 'far fa-file-alt',
'xml' => 'fas fa-code', 'xml' => 'fas fa-code',
// Misc // Misc
'pdf' => 'far fa-file-pdf', 'pdf' => 'far fa-file-pdf',
'lic' => 'far fa-save', 'lic' => 'far fa-save',
@@ -1543,11 +1554,6 @@ class Helper
// return to previous page // return to previous page
if ($redirect_option === 'back') { if ($redirect_option === 'back') {
if ($backUrl === route('home')) {
return redirect()->to($backUrl)
->with('warning', trans('general.page_error'));
}
return redirect()->to($backUrl); return redirect()->to($backUrl);
} }

View File

@@ -43,6 +43,8 @@ class IconHelper
return 'fa-regular fa-envelope'; return 'fa-regular fa-envelope';
case 'phone': case 'phone':
return 'fa-solid fa-phone'; return 'fa-solid fa-phone';
case 'mobile':
return 'fas fa-mobile-screen-button';
case 'long-arrow-right': case 'long-arrow-right':
return 'fas fa-long-arrow-alt-right'; return 'fas fa-long-arrow-alt-right';
case 'download': case 'download':
@@ -151,6 +153,7 @@ class IconHelper
case 'location': case 'location':
return 'fas fa-map-marker-alt'; return 'fas fa-map-marker-alt';
case 'superadmin': case 'superadmin':
case 'admin':
return 'fas fa-crown'; return 'fas fa-crown';
case 'print': case 'print':
return 'fa-solid fa-print'; return 'fa-solid fa-print';

View File

@@ -27,6 +27,45 @@ class StorageHelper
} }
} }
public static function getMediaType($file_with_path) {
// Get the file extension and determine the media type
if (Storage::exists($file_with_path)) {
$fileinfo = pathinfo($file_with_path);
$extension = strtolower($fileinfo['extension']);
switch ($extension) {
case 'avif':
case 'jpg':
case 'png':
case 'gif':
case 'svg':
case 'webp':
return 'image';
case 'pdf':
return 'pdf';
case 'mp3':
case 'wav':
case 'ogg':
return 'audio';
case 'mp4':
case 'webm':
case 'mov':
return 'video';
case 'doc':
case 'docx':
return 'document';
case 'txt':
return 'text';
case 'xls':
case 'xlsx':
case 'ods':
return 'spreadsheet';
default:
return $extension; // Default for unknown types
}
}
return null;
}
/** /**
* This determines the file types that should be allowed inline and checks their fileinfo extension * This determines the file types that should be allowed inline and checks their fileinfo extension
@@ -52,7 +91,6 @@ class StorageHelper
'pdf', 'pdf',
'png', 'png',
'svg', 'svg',
'svg',
'wav', 'wav',
'webm', 'webm',
'webp', 'webp',

View File

@@ -77,9 +77,25 @@ class AccessoriesController extends Controller
$accessory->supplier_id = request('supplier_id'); $accessory->supplier_id = request('supplier_id');
$accessory->notes = request('notes'); $accessory->notes = request('notes');
$accessory = $request->handleImages($accessory); if ($request->has('use_cloned_image')) {
$cloned_model_img = Accessory::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'accessories/'.$new_image_name;
Storage::disk('public')->copy('accessories/'.$cloned_model_img->image, $new_image);
$accessory->image = $new_image_name;
}
} else {
$accessory = $request->handleImages($accessory);
}
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
session()->put(['redirect_option' => $request->get('redirect_option')]);
// Was the accessory created? // Was the accessory created?
if ($accessory->save()) { if ($accessory->save()) {
// Redirect to the new accessory page // Redirect to the new accessory page
@@ -114,11 +130,12 @@ class AccessoriesController extends Controller
$this->authorize('create', Accessory::class); $this->authorize('create', Accessory::class);
$cloned = clone $accessory; $cloned = clone $accessory;
$accessory_to_clone = $accessory;
$cloned->id = null; $cloned->id = null;
$cloned->deleted_at = ''; $cloned->deleted_at = '';
$cloned->location_id = null;
return view('accessories/edit') return view('accessories/edit')
->with('cloned_model', $accessory_to_clone)
->with('item', $cloned); ->with('item', $cloned);
} }

View File

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

View File

@@ -232,6 +232,7 @@ class AcceptanceController extends Controller
'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null, 'signature' => ($sig_filename) ? storage_path() . '/private_uploads/signatures/' . $sig_filename : null,
'logo' => $path_logo, 'logo' => $path_logo,
'date_settings' => $branding_settings->date_display_format, 'date_settings' => $branding_settings->date_display_format,
'admin' => auth()->user()->present()?->fullName,
]; ];
if ($pdf_view_route!='') { if ($pdf_view_route!='') {
@@ -247,15 +248,15 @@ class AcceptanceController extends Controller
// Add the attachment for the signing user into the $data array // Add the attachment for the signing user into the $data array
$data['file'] = $pdf_filename; $data['file'] = $pdf_filename;
$locale = $assigned_user->locale;
try { try {
$assigned_user->notify(new AcceptanceAssetAcceptedToUserNotification($data)); $assigned_user->notify((new AcceptanceAssetAcceptedToUserNotification($data))->locale($locale));
} catch (\Exception $e) { } catch (\Exception $e) {
Log::warning($e); Log::warning($e);
} }
} }
try { try {
$acceptance->notify(new AcceptanceAssetAcceptedNotification($data)); $acceptance->notify((new AcceptanceAssetAcceptedNotification($data))->locale(Setting::getSettings()->locale));
} catch (\Exception $e) { } catch (\Exception $e) {
Log::warning($e); Log::warning($e);
} }
@@ -347,6 +348,7 @@ class AcceptanceController extends Controller
$acceptance->decline($sig_filename, $request->input('note')); $acceptance->decline($sig_filename, $request->input('note'));
$acceptance->notify(new AcceptanceAssetDeclinedNotification($data)); $acceptance->notify(new AcceptanceAssetDeclinedNotification($data));
Log::debug('New event acceptance.');
event(new CheckoutDeclined($acceptance)); event(new CheckoutDeclined($acceptance));
$return_msg = trans('admin/users/message.declined'); $return_msg = trans('admin/users/message.declined');
} }
@@ -356,13 +358,16 @@ class AcceptanceController extends Controller
$recipient = User::find($acceptance->alert_on_response_id); $recipient = User::find($acceptance->alert_on_response_id);
if ($recipient) { if ($recipient) {
Log::debug('Attempting to send email acceptance.');
Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail( Mail::to($recipient)->send(new CheckoutAcceptanceResponseMail(
$acceptance, $acceptance,
$recipient, $recipient,
$request->input('asset_acceptance') === 'accepted', $request->input('asset_acceptance') === 'accepted',
)); ));
Log::debug('Send email notification sucess on checkout acceptance response.');
} }
} catch (Exception $e) { } catch (Exception $e) {
Log::error($e->getMessage());
Log::warning($e); Log::warning($e);
} }
} }

View File

@@ -117,15 +117,20 @@ class AssetsController extends Controller
'jobtitle', 'jobtitle',
]; ];
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
foreach ($all_custom_fields as $field) {
$allowed_columns[] = $field->db_column_name();
}
$filter = []; $filter = [];
if ($request->filled('filter')) { if ($request->filled('filter')) {
$filter = json_decode($request->input('filter'), true); $filter = json_decode($request->input('filter'), true);
}
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load $filter = array_filter($filter, function ($key) use ($allowed_columns) {
foreach ($all_custom_fields as $field) { return in_array($key, $allowed_columns);
$allowed_columns[] = $field->db_column_name(); }, ARRAY_FILTER_USE_KEY);
} }
$assets = Asset::select('assets.*') $assets = Asset::select('assets.*')
@@ -141,6 +146,7 @@ class AssetsController extends Controller
'model.category', 'model.category',
'model.manufacturer', 'model.manufacturer',
'model.fieldset', 'model.fieldset',
'model.depreciation',
'supplier' 'supplier'
); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users. ); // it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users.
@@ -604,7 +610,7 @@ class AssetsController extends Controller
$asset->use_text = $asset->present()->fullName; $asset->use_text = $asset->present()->fullName;
if (($asset->checkedOutToUser()) && ($asset->assigned)) { if (($asset->checkedOutToUser()) && ($asset->assigned)) {
$asset->use_text .= ' → ' . $asset->assigned->getFullNameAttribute(); $asset->use_text .= ' → ' . $asset->assigned->display_name;
} }

View File

@@ -228,11 +228,16 @@ class ConsumablesController extends Controller
foreach ($consumable->consumableAssignments as $consumable_assignment) { foreach ($consumable->consumableAssignments as $consumable_assignment) {
$rows[] = [ $rows[] = [
'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '', 'avatar' => ($consumable_assignment->user) ? e($consumable_assignment->user->present()->gravatar) : '',
'name' => ($consumable_assignment->user) ? $consumable_assignment->user->present()->nameUrl() : 'Deleted User', 'user' => ($consumable_assignment->user) ? [
'id' => (int) $consumable_assignment->user->id,
'name'=> e($consumable_assignment->user->display_name),
] : null,
'created_at' => Helper::getFormattedDateObject($consumable_assignment->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($consumable_assignment->created_at, 'datetime'),
'note' => ($consumable_assignment->note) ? e($consumable_assignment->note) : null, 'note' => ($consumable_assignment->note) ? e($consumable_assignment->note) : null,
'admin' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null, // legacy, so we don't change the shape of the response 'created_by' => ($consumable_assignment->adminuser) ? [
'created_by' => ($consumable_assignment->adminuser) ? $consumable_assignment->adminuser->present()->nameUrl() : null, 'id' => (int) $consumable_assignment->adminuser->id,
'name'=> e($consumable_assignment->adminuser->display_name),
] : null,
]; ];
} }

View File

@@ -195,7 +195,7 @@ class ImportController extends Controller
// Run a backup immediately before processing // Run a backup immediately before processing
if ($request->get('run-backup')) { if ($request->get('run-backup')) {
Log::debug('Backup manually requested via importer'); Log::debug('Backup manually requested via importer');
Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H:i:s')]); Artisan::call('snipeit:backup', ['--filename' => 'pre-import-backup-'.date('Y-m-d-H-i-s')]);
} else { } else {
Log::debug('NO BACKUP requested via importer'); Log::debug('NO BACKUP requested via importer');
} }

View File

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

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
/**
* This class controls all API actions related to notes for
* the Snipe-IT Asset Management application.
*/
class NotesController extends Controller
{
/**
* Retrieve a list of manual notes (action logs) for a given asset.
*
* Checks authorization to view assets, attempts to find the asset by ID,
* and fetches related action log entries of type 'note added', including
* user information for each note. Returns a JSON response with the notes or errors.
*
* @param \Illuminate\Http\Request $request The incoming HTTP request.
* @param Asset $asset The ID of the asset whose notes to retrieve.
* @return \Illuminate\Http\JsonResponse
*/
public function index(Asset $asset): JsonResponse
{
$this->authorize('view', $asset);
// Get the manual notes for the asset
$notes = ActionLog::with('user:id,username')
->where('item_type', Asset::class)
->where('item_id', $asset->id)
->where('action_type', 'note added')
->orderBy('created_at', 'desc')
->get(['id', 'created_at', 'note', 'created_by', 'item_id', 'item_type', 'action_type', 'target_id', 'target_type']);
$notesArray = $notes->map(function ($note) {
return [
'id' => $note->id,
'created_at' => $note->created_at,
'note' => $note->note,
'created_by' => $note->created_by,
'username' => $note->user?->username, // adding the username
'item_id' => $note->item_id,
'item_type' => $note->item_type,
'action_type' => $note->action_type,
];
});
// Return a success response
return response()->json(Helper::formatStandardApiResponse('success', ['notes' => $notesArray, 'asset_id' => $asset->id]));
}
/**
* Store a manual note on a specified asset and log the action.
*
* Checks authorization for updating assets, validates the presence of the 'note',
* attempts to find the asset by ID, and creates a new ActionLog entry if successful.
* Returns JSON responses indicating success or failure with appropriate HTTP status codes.
*
* @param \Illuminate\Http\Request $request The incoming HTTP request containing the 'note'.
* @param Asset $asset The ID of the asset to attach the note to.
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request, Asset $asset): JsonResponse
{
$this->authorize('update', $asset);
if ($request->input('note', '') == '') {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('validation.required', ['attribute' => 'note'])), 422);
}
// Create the note
$logaction = new ActionLog();
$logaction->item_type = get_class($asset);
$logaction->created_by = Auth::id();
$logaction->item_id = $asset->id;
$logaction->note = $request->input('note', '');
if ($logaction->logaction('note added')) {
// Return a success response
return response()->json(Helper::formatStandardApiResponse('success', ['note' => $logaction->note, 'item_id' => $asset->id], trans('general.note_added')));
}
// Return an error response if something went wrong
return response()->json(Helper::formatStandardApiResponse('error', null, 'Something went wrong'), 500);
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Http\Transformers\DatatablesTransformer; use App\Http\Transformers\DatatablesTransformer;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@@ -51,10 +50,22 @@ class SettingsController extends Controller
})->slice(0, 10)->map(function ($item) use ($settings) { })->slice(0, 10)->map(function ($item) use ($settings) {
return (object) [ return (object) [
'username' => $item[$settings['ldap_username_field']][0] ?? null, 'username' => $item[$settings['ldap_username_field']][0] ?? null,
'display_name' => $item[$settings['ldap_display_name']][0] ?? null,
'employee_number' => $item[$settings['ldap_emp_num']][0] ?? null, 'employee_number' => $item[$settings['ldap_emp_num']][0] ?? null,
'lastname' => $item[$settings['ldap_lname_field']][0] ?? null, 'lastname' => $item[$settings['ldap_lname_field']][0] ?? null,
'firstname' => $item[$settings['ldap_fname_field']][0] ?? null, 'firstname' => $item[$settings['ldap_fname_field']][0] ?? null,
'email' => $item[$settings['ldap_email']][0] ?? null, 'email' => $item[$settings['ldap_email']][0] ?? null,
'phone' => $item[$settings['ldap_phone_field']][0] ?? null,
'mobile' => $item[$settings['ldap_mobile']][0] ?? null,
'jobtitle' => $item[$settings['ldap_jobtitle']][0] ?? null,
'department' => $item[$settings['ldap_department']][0] ?? null,
'manager' => $item[$settings['ldap_manager']][0] ?? null,
'address' => $item[$settings['ldap_address']][0] ?? null,
'city' => $item[$settings['ldap_city']][0] ?? null,
'state' => $item[$settings['ldap_state']][0] ?? null,
'zip' => $item[$settings['ldap_zip']][0] ?? null,
'country' => $item[$settings['ldap_country']][0] ?? null,
'location' => $item[$settings['ldap_location']][0] ?? null,
]; ];
}); });
if ($users->count() > 0) { if ($users->count() > 0) {
@@ -78,7 +89,7 @@ class SettingsController extends Controller
} }
} catch (\Exception $e) { } catch (\Exception $e) {
Log::debug('Connection failed but we cannot debug it any further on our end.'); Log::debug('Connection failed but we cannot debug it any further on our end.');
return response()->json(['message' => $e->getMessage()], 500); return response()->json(['message' => $e->getMessage()], 400);
} }
@@ -150,8 +161,11 @@ class SettingsController extends Controller
if (!config('app.lock_passwords')) { if (!config('app.lock_passwords')) {
try { try {
Notification::send(Setting::first(), new MailTest()); Notification::send(Setting::first(), new MailTest());
Log::debug('Attempting to sending to '.config('mail.reply_to.address'));
return response()->json(['message' => 'Mail sent to '.config('mail.reply_to.address')], 200); return response()->json(['message' => 'Mail sent to '.config('mail.reply_to.address')], 200);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Mail sent error using '.config('mail.reply_to.address') .': '. $e->getMessage());
Log::debug($e);
return response()->json(['message' => $e->getMessage()], 500); return response()->json(['message' => $e->getMessage()], 500);
} }
} }
@@ -315,4 +329,4 @@ class SettingsController extends Controller
} }
} }

View File

@@ -194,7 +194,7 @@ class SuppliersController extends Controller
public function destroy($id) : JsonResponse public function destroy($id) : JsonResponse
{ {
$this->authorize('delete', Supplier::class); $this->authorize('delete', Supplier::class);
$supplier = Supplier::with('asset_maintenances', 'assets', 'licenses')->withCount('asset_maintenances as asset_maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id); $supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->findOrFail($id);
$this->authorize('delete', $supplier); $this->authorize('delete', $supplier);
@@ -202,8 +202,8 @@ class SuppliersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count]))); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count])));
} }
if ($supplier->asset_maintenances_count > 0) { if ($supplier->maintenances_count > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count]))); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count])));
} }
if ($supplier->licenses_count > 0) { if ($supplier->licenses_count > 0) {

View File

@@ -7,18 +7,9 @@ use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest; use App\Http\Requests\UploadFileRequest;
use App\Http\Transformers\UploadedFilesTransformer; use App\Http\Transformers\UploadedFilesTransformer;
use App\Models\Accessory;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Location;
use App\Models\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -28,45 +19,6 @@ class UploadedFilesController extends Controller
{ {
static $map_object_type = [
'accessories' => Accessory::class,
'assets' => Asset::class,
'components' => Component::class,
'consumables' => Consumable::class,
'hardware' => Asset::class,
'licenses' => License::class,
'locations' => Location::class,
'models' => AssetModel::class,
'users' => User::class,
];
static $map_storage_path = [
'accessories' => 'private_uploads/accessories/',
'assets' => 'private_uploads/assets/',
'components' => 'private_uploads/components/',
'consumables' => 'private_uploads/consumables/',
'hardware' => 'private_uploads/assets/',
'licenses' => 'private_uploads/licenses/',
'locations' => 'private_uploads/locations/',
'models' => 'private_uploads/assetmodels/',
'users' => 'private_uploads/users/',
];
static $map_file_prefix= [
'accessories' => 'accessory',
'assets' => 'asset',
'components' => 'component',
'consumables' => 'consumable',
'hardware' => 'asset',
'licenses' => 'license',
'locations' => 'location',
'models' => 'model',
'users' => 'user',
];
/** /**
* List files for an object * List files for an object
* *
@@ -93,19 +45,27 @@ class UploadedFilesController extends Controller
'id', 'id',
'filename', 'filename',
'action_type', 'action_type',
'action_date',
'note', 'note',
'created_at', 'created_at',
]; ];
$uploads = $object->uploads();
$offset = ($request->input('offset') > $object->count()) ? $object->count() : abs($request->input('offset')); $uploads = Actionlog::select('action_logs.*')
->whereNotNull('filename')
->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)
->where('action_type', '=', 'uploaded')
->with('adminuser');
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset'));
$limit = app('api_limit_value'); $limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc'; $order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'action_logs.created_at'; $sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
// Text search on action_logs fields // Text search on action_logs fields
// We could use the normal Actionlogs text scope, but it's a very heavy query since it's searcghing across all relations // We could use the normal Actionlogs text scope, but it's a very heavy query since it's searching across all relations
// And we generally won't need that here // and we generally won't need that here
if ($request->filled('search')) { if ($request->filled('search')) {
$uploads->where( $uploads->where(
@@ -116,8 +76,10 @@ class UploadedFilesController extends Controller
); );
} }
$total = $uploads->count();
$uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get(); $uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get();
return (new UploadedFilesTransformer())->transformFiles($uploads, $uploads->count());
return (new UploadedFilesTransformer())->transformFiles($uploads, $total);
} }

View File

@@ -20,9 +20,12 @@ use App\Models\Consumable;
use App\Models\License; use App\Models\License;
use App\Models\User; use App\Models\User;
use App\Notifications\CurrentInventory; use App\Notifications\CurrentInventory;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@@ -62,12 +65,14 @@ class UsersController extends Controller
'users.jobtitle', 'users.jobtitle',
'users.last_login', 'users.last_login',
'users.last_name', 'users.last_name',
'users.display_name',
'users.locale', 'users.locale',
'users.location_id', 'users.location_id',
'users.manager_id', 'users.manager_id',
'users.notes', 'users.notes',
'users.permissions', 'users.permissions',
'users.phone', 'users.phone',
'users.mobile',
'users.state', 'users.state',
'users.two_factor_enrolled', 'users.two_factor_enrolled',
'users.two_factor_optin', 'users.two_factor_optin',
@@ -81,7 +86,12 @@ class UsersController extends Controller
'users.autoassign_licenses', 'users.autoassign_licenses',
'users.website', 'users.website',
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations', 'eulas') ])->with('manager')
->with('groups')
->with('userloc')
->with('company')
->with('department')
->with('createdBy')
->withCount([ ->withCount([
'assets as assets_count' => function(Builder $query) { 'assets as assets_count' => function(Builder $query) {
$query->withoutTrashed(); $query->withoutTrashed();
@@ -102,10 +112,26 @@ class UsersController extends Controller
$users = $users->where('users.activated', '=', $request->input('activated')); $users = $users->where('users.activated', '=', $request->input('activated'));
} }
if ($request->input('admins') == 'true') {
$users = $users->OnlyAdminsAndSuperAdmins();
}
if ($request->input('superadmins') == 'true') {
$users = $users->OnlySuperAdmins();
}
if ($request->filled('company_id')) { if ($request->filled('company_id')) {
$users = $users->where('users.company_id', '=', $request->input('company_id')); $users = $users->where('users.company_id', '=', $request->input('company_id'));
} }
if ($request->filled('phone')) {
$users = $users->where('users.phone', '=', $request->input('phone'));
}
if ($request->filled('mobile')) {
$users = $users->where('users.mobile', '=', $request->input('mobile'));
}
if ($request->filled('location_id')) { if ($request->filled('location_id')) {
$users = $users->where('users.location_id', '=', $request->input('location_id')); $users = $users->where('users.location_id', '=', $request->input('location_id'));
} }
@@ -130,6 +156,10 @@ class UsersController extends Controller
$users = $users->where('users.last_name', '=', $request->input('last_name')); $users = $users->where('users.last_name', '=', $request->input('last_name'));
} }
if ($request->filled('display_name')) {
$users = $users->where('users.display_name', '=', $request->input('display_name'));
}
if ($request->filled('employee_num')) { if ($request->filled('employee_num')) {
$users = $users->where('users.employee_num', '=', $request->input('employee_num')); $users = $users->where('users.employee_num', '=', $request->input('employee_num'));
} }
@@ -260,6 +290,7 @@ class UsersController extends Controller
[ [
'last_name', 'last_name',
'first_name', 'first_name',
'display_name',
'email', 'email',
'jobtitle', 'jobtitle',
'username', 'username',
@@ -278,6 +309,7 @@ class UsersController extends Controller
'manages_users_count', 'manages_users_count',
'manages_locations_count', 'manages_locations_count',
'phone', 'phone',
'mobile',
'address', 'address',
'city', 'city',
'state', 'state',
@@ -330,6 +362,7 @@ class UsersController extends Controller
'users.employee_num', 'users.employee_num',
'users.first_name', 'users.first_name',
'users.last_name', 'users.last_name',
'users.display_name',
'users.gravatar', 'users.gravatar',
'users.avatar', 'users.avatar',
'users.email', 'users.email',
@@ -340,20 +373,17 @@ class UsersController extends Controller
$users = $users->where(function ($query) use ($request) { $users = $users->where(function ($query) use ($request) {
$query->SimpleNameSearch($request->get('search')) $query->SimpleNameSearch($request->get('search'))
->orWhere('username', 'LIKE', '%'.$request->get('search').'%') ->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
->orWhere('display_name', 'LIKE', '%'.$request->get('search').'%')
->orWhere('email', 'LIKE', '%'.$request->get('search').'%') ->orWhere('email', 'LIKE', '%'.$request->get('search').'%')
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%'); ->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
}); });
} }
$users = $users->orderBy('last_name', 'asc')->orderBy('first_name', 'asc'); $users = $users->orderBy('display_name', 'asc')->orderBy('last_name', 'asc')->orderBy('first_name', 'asc');
$users = $users->paginate(50); $users = $users->paginate(50);
foreach ($users as $user) { foreach ($users as $user) {
$name_str = ''; $name_str = $user->display_name;
if ($user->last_name != '') {
$name_str .= $user->last_name.', ';
}
$name_str .= $user->first_name;
if ($user->username != '') { if ($user->username != '') {
$name_str .= ' ('.$user->username.')'; $name_str .= ' ('.$user->username.')';
@@ -408,6 +438,17 @@ class UsersController extends Controller
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar'); app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
if ($user->save()) { if ($user->save()) {
if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
try {
$user->notify(new WelcomeNotification($user));
} catch (\Exception $e) {
Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
}
}
if ($request->filled('groups')) { if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups')); $user->groups()->sync($request->input('groups'));
} else { } else {
@@ -475,8 +516,29 @@ class UsersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager')); return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
} }
if ($request->filled('password')) { // check for permissions related fields and pull them out if the current user cannot edit them
$user->password = bcrypt($request->input('password')); if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
if ($request->filled('username')) {
$user->username = $request->input('username');
}
if ($request->filled('display_name')) {
$user->display_name = $request->input('display_name');
}
if ($request->filled('email')) {
$user->email = $request->input('email');
}
if ($request->filled('activated')) {
$user->activated = $request->input('activated');
}
} }
// We need to use has() instead of filled() // We need to use has() instead of filled()
@@ -791,4 +853,37 @@ class UsersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200); return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);
} }
/**
* Run the LDAP sync command to import users from LDAP via API.
*
* @author A. Gianotto <snipe@snipe.net>
* @since 8.2.2
*
* @return \Illuminate\Http\JsonResponse
*/
public function syncLdapUsers(Request $request)
{
$this->authorize('update', User::class);
// Call Artisan LDAP import command.
Artisan::call('snipeit:ldap-sync', ['--location_id' => $request->input('location_id'), '--json_summary' => true]);
// Collect and parse JSON summary.
$ldap_results_json = Artisan::output();
$ldap_results = json_decode($ldap_results_json, true);
if (!$ldap_results) {
return response()->json(Helper::formatStandardApiResponse('error', null,trans('general.no_results')), 200);
}
// Direct user to appropriate status page.
if ($ldap_results['error']) {
return response()->json(Helper::formatStandardApiResponse('error', null, $ldap_results['error_message']), 200);
}
return response()->json(Helper::formatStandardApiResponse('success', null, $ldap_results['summary']), 200);
}
} }

View File

@@ -87,7 +87,20 @@ class AssetModelsController extends Controller
$model->fieldset_id = $request->input('fieldset_id'); $model->fieldset_id = $request->input('fieldset_id');
} }
$model = $request->handleImages($model); if ($request->has('use_cloned_image')) {
$cloned_model_img = AssetModel::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'models/'.$new_image_name;
Storage::disk('public')->copy('models/'.$cloned_model_img->image, $new_image);
$model->image = $new_image_name;
}
} else {
$model = $request->handleImages($model);
}
if ($model->save()) { if ($model->save()) {
if ($this->shouldAddDefaultValues($request->input())) { if ($this->shouldAddDefaultValues($request->input())) {
@@ -271,7 +284,7 @@ class AssetModelsController extends Controller
->with('depreciation_list', Helper::depreciationList()) ->with('depreciation_list', Helper::depreciationList())
->with('item', $model) ->with('item', $model)
->with('model_id', $model->id) ->with('model_id', $model->id)
->with('clone_model', $cloned_model); ->with('cloned_model', $cloned_model);
} }

View File

@@ -1,115 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\StorageHelper;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\AssetModel;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use \Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class AssetModelsFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @param UploadFileRequest $request
* @param int $modelId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, $modelId = null) : RedirectResponse
{
if (! $model = AssetModel::find($modelId)) {
return redirect()->route('models.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('update', $model);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/assetmodels')) {
Storage::makeDirectory('private_uploads/assetmodels', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$model->id,$file);
$model->logUpload($file_name, $request->get('notes'));
}
return redirect()->back()->withFragment('files')->with('success', trans('general.file_upload_success'));
}
return redirect()->back()->withFragment('files')->with('error', trans('admin/hardware/message.upload.nofiles'));
}
/**
* Check for permissions and display the file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $modelId
* @param int $fileId
* @since [v1.0]
*/
public function show(AssetModel $model, $fileId = null) : StreamedResponse | Response | RedirectResponse | BinaryFileResponse
{
$this->authorize('view', $model);
if (! $log = Actionlog::find($fileId)) {
return response('No matching record for that model/file', 500)
->header('Content-Type', 'text/plain');
}
$file = 'private_uploads/assetmodels/'.$log->filename;
if (! Storage::exists($file)) {
return response('File '.$file.' not found on server', 404)
->header('Content-Type', 'text/plain');
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
/**
* Delete the associated file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $modelId
* @param int $fileId
* @since [v1.0]
*/
public function destroy(AssetModel $model, $fileId = null) : RedirectResponse
{
$rel_path = 'private_uploads/assetmodels';
$this->authorize('update', $model);
$log = Actionlog::find($fileId);
if ($log) {
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
$log->delete();
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
}
}

View File

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

View File

@@ -157,8 +157,16 @@ class AssetsController extends Controller
$asset->location_id = $request->input('rtd_location_id', null); $asset->location_id = $request->input('rtd_location_id', null);
} }
// Create the image (if one was chosen.) if ($request->has('use_cloned_image')) {
if ($request->has('image')) { $cloned_model_img = Asset::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'assets/'.$new_image_name;
Storage::disk('public')->copy('assets/'.$cloned_model_img->image, $new_image);
$asset->image = $new_image_name;
}
} else {
$asset = $request->handleImages($asset); $asset = $request->handleImages($asset);
} }
@@ -226,8 +234,15 @@ class AssetsController extends Controller
$failures[] = join(",", $asset->getErrors()->all()); $failures[] = join(",", $asset->getErrors()->all());
} }
} }
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
session()->put(['checkout_to_type' => $request->get('checkout_to_type'),
'other_redirect' => 'model' ]);
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
if ($successes) { if ($successes) {
@@ -409,6 +424,9 @@ class AssetsController extends Controller
$model = AssetModel::find($request->get('model_id')); $model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) { if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) { foreach ($model->fieldset->fields as $field) {
if ($field->element == 'checkbox' && !$request->has($field->db_column)) {
$asset->{$field->db_column} = null;
}
if ($request->has($field->db_column)) { if ($request->has($field->db_column)) {
if ($field->field_encrypted == '1') { if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) { if (Gate::allows('assets.view.encrypted_custom_fields')) {
@@ -641,8 +659,9 @@ class AssetsController extends Controller
*/ */
public function getClone(Asset $asset) public function getClone(Asset $asset)
{ {
$this->authorize('create', $asset); $this->authorize('create', Asset::class);
$cloned = clone $asset; $cloned = clone $asset;
$cloned_model = $asset;
$cloned->id = null; $cloned->id = null;
$cloned->asset_tag = ''; $cloned->asset_tag = '';
$cloned->serial = ''; $cloned->serial = '';
@@ -652,6 +671,7 @@ class AssetsController extends Controller
return view('hardware/edit') return view('hardware/edit')
->with('statuslabel_list', Helper::statusLabelList()) ->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList()) ->with('statuslabel_types', Helper::statusTypeList())
->with('cloned_model', $cloned_model)
->with('item', $cloned); ->with('item', $cloned);
} }
@@ -777,7 +797,7 @@ class AssetsController extends Controller
'item_id' => $asset->id, 'item_id' => $asset->id,
'item_type' => Asset::class, 'item_type' => Asset::class,
'created_by' => auth()->id(), 'created_by' => auth()->id(),
'note' => 'Checkout imported by '.auth()->user()->present()->fullName().' from history importer', 'note' => 'Checkout imported by '.auth()->user()->display_name.' from history importer',
'target_id' => $item[$asset_tag][$batch_counter]['user_id'], 'target_id' => $item[$asset_tag][$batch_counter]['user_id'],
'target_type' => User::class, 'target_type' => User::class,
'created_at' => $item[$asset_tag][$batch_counter]['checkout_date'], 'created_at' => $item[$asset_tag][$batch_counter]['checkout_date'],
@@ -805,7 +825,7 @@ class AssetsController extends Controller
'item_id' => $item[$asset_tag][$batch_counter]['asset_id'], 'item_id' => $item[$asset_tag][$batch_counter]['asset_id'],
'item_type' => Asset::class, 'item_type' => Asset::class,
'created_by' => auth()->id(), 'created_by' => auth()->id(),
'note' => 'Checkin imported by '.auth()->user()->present()->fullName().' from history importer', 'note' => 'Checkin imported by '.auth()->user()->display_name.' from history importer',
'target_id' => null, 'target_id' => null,
'created_at' => $checkin_date, 'created_at' => $checkin_date,
'action_type' => 'checkin', 'action_type' => 'checkin',

View File

@@ -161,6 +161,7 @@ class BulkAssetsController extends Controller
$models = $assets->unique('model_id'); $models = $assets->unique('model_id');
$modelNames = []; $modelNames = [];
foreach($models as $model) { foreach($models as $model) {
$modelNames[] = $model->model->name; $modelNames[] = $model->model->name;
} }
@@ -196,7 +197,6 @@ class BulkAssetsController extends Controller
case 'edit': case 'edit':
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
return view('hardware/bulk') return view('hardware/bulk')
->with('assets', $asset_ids) ->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList()) ->with('statuslabel_list', Helper::statusLabelList())
@@ -224,11 +224,8 @@ class BulkAssetsController extends Controller
$error_array = array(); $error_array = array();
// Get the back url from the session and then destroy the session // Get the back url from the session and then destroy the session
$bulk_back_url = route('hardware.index');
if ($request->session()->has('bulk_back_url')) { $bulk_back_url = $request->session()->pull('bulk_back_url', url()->previous());
$bulk_back_url = $request->session()->pull('bulk_back_url');
}
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray(); $custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
@@ -543,7 +540,13 @@ class BulkAssetsController extends Controller
} // end asset foreach } // end asset foreach
if ($has_errors > 0) { if ($has_errors > 0) {
return redirect($bulk_back_url)->with('bulk_asset_errors', $error_array); session()->put('bulkedit_ids', $request->input('ids'));
session()->put('bulk_asset_errors',$error_array);
return redirect()
->route('hardware.index')
->with('bulk_asset_errors', $error_array)
->withInput();
} }
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success')); return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success'));
@@ -735,4 +738,33 @@ class BulkAssetsController extends Controller
} }
return false; return false;
} }
public function bulkEditForm(): View|RedirectResponse
{
$this->authorize('update', Asset::class);
$asset_ids = session()->pull('bulkedit_ids', []);
if (empty($asset_ids)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.update.no_assets_selected'));
}
$assets = Asset::with('model')->withTrashed()->whereIn('id', $asset_ids)->get();
if ($assets->isEmpty()) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.update.assets_do_not_exist_or_are_invalid'));
}
$models = $assets->unique('model_id');
$modelNames = [];
foreach ($models as $model) {
$modelNames[] = $model->model->name;
}
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList())
->with('models', $models->pluck(['model']))
->with('modelNames', $modelNames);
}
} }

View File

@@ -88,7 +88,12 @@ class ComponentsController extends Controller
$component = $request->handleImages($component); $component = $request->handleImages($component);
session()->put(['redirect_option' => $request->get('redirect_option')]); if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
if ($component->save()) { if ($component->save()) {
return Helper::getRedirectOption($request, $component->id, 'Components') return Helper::getRedirectOption($request, $component->id, 'Components')

View File

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

View File

@@ -7,7 +7,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use App\Models\Company; use App\Models\Company;
use App\Models\Consumable; use App\Models\Consumable;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View; use \Illuminate\Contracts\View\View;
@@ -81,13 +81,29 @@ class ConsumablesController extends Controller
$consumable->purchase_date = $request->input('purchase_date'); $consumable->purchase_date = $request->input('purchase_date');
$consumable->purchase_cost = $request->input('purchase_cost'); $consumable->purchase_cost = $request->input('purchase_cost');
$consumable->qty = $request->input('qty'); $consumable->qty = $request->input('qty');
$consumable->created_by = auth()->id(); $consumable->created_by = auth()->id();
$consumable->notes = $request->input('notes'); $consumable->notes = $request->input('notes');
$consumable = $request->handleImages($consumable); if ($request->has('use_cloned_image')) {
$cloned_model_img = Consumable::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'consumables/'.$new_image_name;
Storage::disk('public')->copy('consumables/'.$cloned_model_img->image, $new_image);
$consumable->image = $new_image_name;
}
} else {
$consumable = $request->handleImages($consumable);
}
if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
session()->put(['redirect_option' => $request->get('redirect_option')]);
if ($consumable->save()) { if ($consumable->save()) {
return Helper::getRedirectOption($request, $consumable->id, 'Consumables') return Helper::getRedirectOption($request, $consumable->id, 'Consumables')
@@ -213,9 +229,10 @@ class ConsumablesController extends Controller
$consumable_to_close = $consumable; $consumable_to_close = $consumable;
$consumable = clone $consumable_to_close; $consumable = clone $consumable_to_close;
$consumable->id = null; $consumable->id = null;
$consumable->image = null;
$consumable->created_by = null; $consumable->created_by = null;
return view('consumables/edit')->with('item', $consumable); return view('consumables/edit')
->with('cloned_model', $consumable_to_close)
->with('item', $consumable);
} }
} }

View File

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

View File

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

View File

@@ -144,10 +144,9 @@ class CustomFieldsController extends Controller
*/ */
public function deleteFieldFromFieldset($field_id, $fieldset_id) : RedirectResponse public function deleteFieldFromFieldset($field_id, $fieldset_id) : RedirectResponse
{ {
$this->authorize('update', CustomField::class);
$field = CustomField::find($field_id); $field = CustomField::find($field_id);
$this->authorize('update', $field);
// Check that the field exists - this is mostly related to the demo, where we // Check that the field exists - this is mostly related to the demo, where we
// rewrite the data every x minutes, so it's possible someone might be disassociating // rewrite the data every x minutes, so it's possible someone might be disassociating
// a field from a fieldset just as we're wiping the database // a field from a fieldset just as we're wiping the database
@@ -157,11 +156,12 @@ class CustomFieldsController extends Controller
return redirect()->route('fieldsets.show', ['fieldset' => $fieldset_id]) return redirect()->route('fieldsets.show', ['fieldset' => $fieldset_id])
->with('success', trans('admin/custom_fields/message.field.delete.success')); ->with('success', trans('admin/custom_fields/message.field.delete.success'));
} else { } else {
return redirect()->back()->withErrors(['message' => "Field is in use and cannot be deleted."]); return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.error'))
->withInput();
} }
} }
return redirect()->back()->withErrors(['message' => "Error deleting field from fieldset"]); return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.error'));
} }
@@ -172,20 +172,16 @@ class CustomFieldsController extends Controller
* @author [Brady Wetherington] [<uberbrady@gmail.com>] * @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v1.8] * @since [v1.8]
*/ */
public function destroy($field_id) : RedirectResponse public function destroy(CustomField $field) : RedirectResponse
{ {
if ($field = CustomField::find($field_id)) { $this->authorize('delete', CustomField::class);
$this->authorize('delete', $field);
if (($field->fieldset) && ($field->fieldset->count() > 0)) { if (($field->fieldset) && ($field->fieldset->count() > 0)) {
return redirect()->back()->withErrors(['message' => 'Field is in-use']); return redirect()->back()->with('error', trans('admin/custom_fields/message.field.delete.in_use'));
}
$field->delete();
return redirect()->route("fields.index")
->with("success", trans('admin/custom_fields/message.field.delete.success'));
} }
$field->delete();
return redirect()->back()->withErrors(['message' => 'Field does not exist']); return redirect()->route("fields.index")
->with("success", trans('admin/custom_fields/message.field.delete.success'));
} }
@@ -198,7 +194,7 @@ class CustomFieldsController extends Controller
*/ */
public function edit(Request $request, CustomField $field) : View | RedirectResponse public function edit(Request $request, CustomField $field) : View | RedirectResponse
{ {
$this->authorize('update', $field); $this->authorize('update', CustomField::class);
$fieldsets = CustomFieldset::get(); $fieldsets = CustomFieldset::get();
$customFormat = ''; $customFormat = '';
if ((stripos($field->format, 'regex') === 0) && ($field->format !== CustomField::PREDEFINED_FORMATS['MAC'])) { if ((stripos($field->format, 'regex') === 0) && ($field->format !== CustomField::PREDEFINED_FORMATS['MAC'])) {
@@ -228,7 +224,7 @@ class CustomFieldsController extends Controller
*/ */
public function update(CustomFieldRequest $request, CustomField $field) : RedirectResponse public function update(CustomFieldRequest $request, CustomField $field) : RedirectResponse
{ {
$this->authorize('update', $field); $this->authorize('update', CustomField::class);
$show_in_email = $request->get("show_in_email", 0); $show_in_email = $request->get("show_in_email", 0);
$display_in_user_view = $request->get("display_in_user_view", 0); $display_in_user_view = $request->get("display_in_user_view", 0);
@@ -265,7 +261,6 @@ class CustomFieldsController extends Controller
if ($field->save()) { if ($field->save()) {
// Sync fields with fieldsets // Sync fields with fieldsets
$fieldset_array = $request->input('associate_fieldsets'); $fieldset_array = $request->input('associate_fieldsets');
if ($request->has('associate_fieldsets') && (is_array($fieldset_array))) { if ($request->has('associate_fieldsets') && (is_array($fieldset_array))) {

View File

@@ -87,7 +87,9 @@ class LicenseCheckinController extends Controller
if($licenseSeat->assigned_to != null){ if($licenseSeat->assigned_to != null){
$return_to = User::withTrashed()->find($licenseSeat->assigned_to); $return_to = User::withTrashed()->find($licenseSeat->assigned_to);
session()->put('checkedInFrom', $return_to->id); if ($return_to) {
session()->put('checkedInFrom', $return_to->id);
}
} else { } else {
$return_to = Asset::find($licenseSeat->asset_id); $return_to = Asset::find($licenseSeat->asset_id);
} }

View File

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

View File

@@ -102,7 +102,11 @@ class LicensesController extends Controller
$license->created_by = auth()->id(); $license->created_by = auth()->id();
$license->min_amt = $request->input('min_amt'); $license->min_amt = $request->input('min_amt');
session()->put(['redirect_option' => $request->get('redirect_option')]); if($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
if ($license->save()) { if ($license->save()) {
return Helper::getRedirectOption($request, $license->id, 'Licenses') return Helper::getRedirectOption($request, $license->id, 'Licenses')
@@ -304,13 +308,16 @@ class LicensesController extends Controller
$response = new StreamedResponse(function () { $response = new StreamedResponse(function () {
// Open output stream // Open output stream
$handle = fopen('php://output', 'w'); $handle = fopen('php://output', 'w');
$licenses= License::with('company', $licenses = License::with('company',
'manufacturer', 'manufacturer',
'category', 'category',
'supplier', 'supplier',
'adminuser', 'adminuser',
'assignedusers') 'assignedusers');
->orderBy('created_at', 'DESC'); if (request()->filled('category_id')) {
$licenses = $licenses->where('category_id', request()->input('category_id'));
}
$licenses = $licenses->orderBy('created_at', 'DESC');
Company::scopeCompanyables($licenses) Company::scopeCompanyables($licenses)
->chunk(500, function ($licenses) use ($handle) { ->chunk(500, function ($licenses) use ($handle) {
$headers = [ $headers = [
@@ -357,7 +364,7 @@ class LicensesController extends Controller
$license->order_number, $license->order_number,
$license->free_seat_count, $license->free_seat_count,
$license->seats, $license->seats,
($license->adminuser ? $license->adminuser->present()->fullName() : trans('admin/reports/general.deleted_user')), ($license->adminuser ? $license->adminuser->display_name : trans('admin/reports/general.deleted_user')),
$license->depreciation ? $license->depreciation->name: '', $license->depreciation ? $license->depreciation->name: '',
$license->updated_at, $license->updated_at,
$license->deleted_at, $license->deleted_at,

View File

@@ -96,7 +96,18 @@ class LocationsController extends Controller
$location->company_id = $request->input('company_id'); $location->company_id = $request->input('company_id');
} }
$location = $request->handleImages($location); if ($request->has('use_cloned_image')) {
$cloned_model_img = Location::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'locations/'.$new_image_name;
Storage::disk('public')->copy('locations/'.$cloned_model_img->image, $new_image);
$location->image = $new_image_name;
}
} else {
$location = $request->handleImages($location);
}
if ($location->save()) { if ($location->save()) {
return redirect()->route('locations.index')->with('success', trans('admin/locations/message.create.success')); return redirect()->route('locations.index')->with('success', trans('admin/locations/message.create.success'));
@@ -275,9 +286,9 @@ class LocationsController extends Controller
// unset these values // unset these values
$location->id = null; $location->id = null;
$location->image = null;
return view('locations/edit') return view('locations/edit')
->with('cloned_model', $location_to_clone)
->with('item', $location); ->with('item', $location);
} }

View File

@@ -1,111 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\StorageHelper;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\Location;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use \Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class LocationsFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @param UploadFileRequest $request
* @param int $modelId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*@since [v1.0]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, Location $location) : RedirectResponse
{
$this->authorize('update', $location);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/locations')) {
Storage::makeDirectory('private_uploads/locations', 775);
}
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/locations/','location-'.$location->id, $file);
$location->logUpload($file_name, $request->get('notes'));
}
return redirect()->back()->withFragment('files')->with('success', trans('general.file_upload_success'));
}
return redirect()->back()->withFragment('files')->with('error', trans('admin/hardware/message.upload.nofiles'));
}
/**
* Check for permissions and display the file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $modelId
* @param int $fileId
* @since [v1.0]
*/
public function show(Location $location, $fileId = null) : StreamedResponse | Response | RedirectResponse | BinaryFileResponse
{
$this->authorize('view', $location);
if (! $log = Actionlog::find($fileId)) {
return redirect()->back()->withFragment('files')->with('error', 'No matching file record');
}
$file = 'private_uploads/locations/'.$log->filename;
if (! Storage::exists($file)) {
return redirect()->back()->withFragment('files')->with('error', 'No matching file on server');
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
/**
* Delete the associated file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $modelId
* @param int $fileId
* @since [v1.0]
*/
public function destroy(Location $location, $fileId = null) : RedirectResponse
{
$rel_path = 'private_uploads/locations';
$this->authorize('update', $location);
$log = Actionlog::find($fileId);
if ($log) {
// This should be moved to purge
// if (Storage::exists($rel_path.'/'.$log->filename)) {
// Storage::delete($rel_path.'/'.$log->filename);
// }
$log->delete();
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.deletefile.success'));
}
}

View File

@@ -2,8 +2,9 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Asset; use App\Models\Asset;
use App\Models\AssetMaintenance; use App\Models\Maintenance;
use App\Models\Company; use App\Models\Company;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Carbon\Carbon; use Carbon\Carbon;
@@ -17,29 +18,23 @@ use \Illuminate\Http\RedirectResponse;
* *
* @version v2.0 * @version v2.0
*/ */
class AssetMaintenancesController extends Controller class MaintenancesController extends Controller
{ {
/** /**
* Returns a view that invokes the ajax tables which actually contains * Returns a view that invokes the ajax tables which actually contains
* the content for the asset maintenances listing, which is generated in getDatatable. * the content for the asset maintenances listing.
*
* @todo This should be replaced with middleware and/or policies
* @see AssetMaintenancesController::getDatatable() method that generates the JSON response
* @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0
* @since [v1.8]
*/ */
public function index() : View public function index() : View
{ {
$this->authorize('view', Asset::class); $this->authorize('view', Asset::class);
return view('asset_maintenances/index'); return view('maintenances.index');
} }
/** /**
* Returns a form view to create a new asset maintenance. * Returns a form view to create a new asset maintenance.
* *
* @see AssetMaintenancesController::postCreate() method that stores the data * @see MaintenancesController::postCreate() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0 * @version v1.0
* @since [v1.8] * @since [v1.8]
@@ -55,21 +50,21 @@ class AssetMaintenancesController extends Controller
$asset->asset_id = $asset->id; $asset->asset_id = $asset->id;
} }
return view('asset_maintenances/edit') return view('maintenances/edit')
->with('assetMaintenanceType', AssetMaintenance::getImprovementOptions()) ->with('maintenanceType', Maintenance::getImprovementOptions())
->with('asset', $asset) ->with('asset', $asset)
->with('item', new AssetMaintenance); ->with('item', new Maintenance);
} }
/** /**
* Validates and stores the new asset maintenance * Validates and stores the new asset maintenance
* *
* @see AssetMaintenancesController::getCreate() method for the form * @see MaintenancesController::getCreate() method for the form
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0 * @version v1.0
* @since [v1.8] * @since [v1.8]
*/ */
public function store(Request $request) : RedirectResponse public function store(ImageUploadRequest $request) : RedirectResponse
{ {
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
@@ -78,72 +73,73 @@ class AssetMaintenancesController extends Controller
// Loop through the selected assets // Loop through the selected assets
foreach ($assets as $asset) { foreach ($assets as $asset) {
$assetMaintenance = new AssetMaintenance(); $maintenance = new Maintenance();
$assetMaintenance->supplier_id = $request->input('supplier_id'); $maintenance->supplier_id = $request->input('supplier_id');
$assetMaintenance->is_warranty = $request->input('is_warranty'); $maintenance->is_warranty = $request->input('is_warranty');
$assetMaintenance->cost = $request->input('cost'); $maintenance->cost = $request->input('cost');
$assetMaintenance->notes = $request->input('notes'); $maintenance->notes = $request->input('notes');
// Save the asset maintenance data // Save the asset maintenance data
$assetMaintenance->asset_id = $asset->id; $maintenance->asset_id = $asset->id;
$assetMaintenance->asset_maintenance_type = $request->input('asset_maintenance_type'); $maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$assetMaintenance->title = $request->input('title'); $maintenance->name = $request->input('name');
$assetMaintenance->start_date = $request->input('start_date'); $maintenance->start_date = $request->input('start_date');
$assetMaintenance->completion_date = $request->input('completion_date'); $maintenance->completion_date = $request->input('completion_date');
$assetMaintenance->created_by = auth()->id(); $maintenance->created_by = auth()->id();
if (($assetMaintenance->completion_date !== null) if (($maintenance->completion_date !== null)
&& ($assetMaintenance->start_date !== '') && ($maintenance->start_date !== '')
&& ($assetMaintenance->start_date !== '0000-00-00') && ($maintenance->start_date !== '0000-00-00')
) { ) {
$startDate = Carbon::parse($assetMaintenance->start_date); $startDate = Carbon::parse($maintenance->start_date);
$completionDate = Carbon::parse($assetMaintenance->completion_date); $completionDate = Carbon::parse($maintenance->completion_date);
$assetMaintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true); $maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
} }
$maintenance = $request->handleImages($maintenance);
// Was the asset maintenance created? // Was the asset maintenance created?
if (!$assetMaintenance->save()) { if (!$maintenance->save()) {
return redirect()->back()->withInput()->withErrors($assetMaintenance->getErrors()); return redirect()->back()->withInput()->withErrors($maintenance->getErrors());
} }
} }
return redirect()->route('maintenances.index') return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.create.success')); ->with('success', trans('admin/maintenances/message.create.success'));
} }
/** /**
* Returns a form view to edit a selected asset maintenance. * Returns a form view to edit a selected asset maintenance.
* *
* @see AssetMaintenancesController::postEdit() method that stores the data * @see MaintenancesController::postEdit() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0 * @version v1.0
* @since [v1.8] * @since [v1.8]
*/ */
public function edit(AssetMaintenance $maintenance) : View | RedirectResponse public function edit(Maintenance $maintenance) : View | RedirectResponse
{ {
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
$this->authorize('update', $maintenance->asset); $this->authorize('update', $maintenance->asset);
return view('asset_maintenances/edit') return view('maintenances/edit')
->with('selected_assets', $maintenance->asset->pluck('id')->toArray()) ->with('selected_assets', $maintenance->asset->pluck('id')->toArray())
->with('asset_ids', request()->input('asset_ids', [])) ->with('asset_ids', request()->input('asset_ids', []))
->with('assetMaintenanceType', AssetMaintenance::getImprovementOptions()) ->with('maintenanceType', Maintenance::getImprovementOptions())
->with('item', $maintenance); ->with('item', $maintenance);
} }
/** /**
* Validates and stores an update to an asset maintenance * Validates and stores an update to an asset maintenance
* *
* @see AssetMaintenancesController::postEdit() method that stores the data * @see MaintenancesController::postEdit() method that stores the data
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @param Request $request * @param Request $request
* @param int $assetMaintenanceId * @param int $maintenanceId
* @version v1.0 * @version v1.0
* @since [v1.8] * @since [v1.8]
*/ */
public function update(Request $request, AssetMaintenance $maintenance) : View | RedirectResponse public function update(ImageUploadRequest $request, Maintenance $maintenance) : View | RedirectResponse
{ {
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
$this->authorize('update', $maintenance->asset); $this->authorize('update', $maintenance->asset);
@@ -153,7 +149,7 @@ class AssetMaintenancesController extends Controller
$maintenance->cost = $request->input('cost'); $maintenance->cost = $request->input('cost');
$maintenance->notes = $request->input('notes'); $maintenance->notes = $request->input('notes');
$maintenance->asset_maintenance_type = $request->input('asset_maintenance_type'); $maintenance->asset_maintenance_type = $request->input('asset_maintenance_type');
$maintenance->title = $request->input('title'); $maintenance->name = $request->input('name');
$maintenance->start_date = $request->input('start_date'); $maintenance->start_date = $request->input('start_date');
$maintenance->completion_date = $request->input('completion_date'); $maintenance->completion_date = $request->input('completion_date');
@@ -176,10 +172,11 @@ class AssetMaintenancesController extends Controller
$completionDate = Carbon::parse($maintenance->completion_date); $completionDate = Carbon::parse($maintenance->completion_date);
$maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true); $maintenance->asset_maintenance_time = (int) $completionDate->diffInDays($startDate, true);
} }
$maintenance = $request->handleImages($maintenance);
if ($maintenance->save()) { if ($maintenance->save()) {
return redirect()->route('maintenances.index') return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.edit.success')); ->with('success', trans('admin/maintenances/message.edit.success'));
} }
return redirect()->back()->withInput()->withErrors($maintenance->getErrors()); return redirect()->back()->withInput()->withErrors($maintenance->getErrors());
@@ -189,11 +186,11 @@ class AssetMaintenancesController extends Controller
* Delete an asset maintenance * Delete an asset maintenance
* *
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @param int $assetMaintenanceId * @param int $maintenanceId
* @version v1.0 * @version v1.0
* @since [v1.8] * @since [v1.8]
*/ */
public function destroy(AssetMaintenance $maintenance) : RedirectResponse public function destroy(Maintenance $maintenance) : RedirectResponse
{ {
$this->authorize('update', Asset::class); $this->authorize('update', Asset::class);
$this->authorize('update', $maintenance->asset); $this->authorize('update', $maintenance->asset);
@@ -201,19 +198,19 @@ class AssetMaintenancesController extends Controller
$maintenance->delete(); $maintenance->delete();
// Redirect to the asset_maintenance management page // Redirect to the asset_maintenance management page
return redirect()->route('maintenances.index') return redirect()->route('maintenances.index')
->with('success', trans('admin/asset_maintenances/message.delete.success')); ->with('success', trans('admin/maintenances/message.delete.success'));
} }
/** /**
* View an asset maintenance * View an asset maintenance
* *
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @param int $assetMaintenanceId * @param int $maintenanceId
* @version v1.0 * @version v1.0
* @since [v1.8] * @since [v1.8]
*/ */
public function show(AssetMaintenance $maintenance) : View | RedirectResponse public function show(Maintenance $maintenance) : View | RedirectResponse
{ {
return view('asset_maintenances/view')->with('assetMaintenance', $maintenance); return view('maintenances.view')->with('maintenance', $maintenance);
} }
} }

View File

@@ -51,7 +51,7 @@ class ManufacturersController extends Controller
$manufacturers_count = Manufacturer::withTrashed()->count(); $manufacturers_count = Manufacturer::withTrashed()->count();
if ($manufacturers_count == 0) { if ($manufacturers_count == 0) {
Artisan::call('db:seed', ['--class' => 'ManufacturerSeeder']); Artisan::call('db:seed', ['--class' => 'Database\\Seeders\\ManufacturerSeeder', '--force' => true]);
return redirect()->route('manufacturers.index')->with('success', trans('general.seeding.manufacturers.success')); return redirect()->route('manufacturers.index')->with('success', trans('general.seeding.manufacturers.success'));
} }

View File

@@ -9,7 +9,7 @@ use App\Models\Actionlog;
use App\Models\Asset; use App\Models\Asset;
use App\Models\AssetModel; use App\Models\AssetModel;
use App\Models\Category; use App\Models\Category;
use App\Models\AssetMaintenance; use App\Models\Maintenance;
use App\Models\CheckoutAcceptance; use App\Models\CheckoutAcceptance;
use App\Models\Company; use App\Models\Company;
use App\Models\CustomField; use App\Models\CustomField;
@@ -17,13 +17,11 @@ use App\Models\Depreciation;
use App\Models\License; use App\Models\License;
use App\Models\ReportTemplate; use App\Models\ReportTemplate;
use App\Models\Setting; use App\Models\Setting;
use App\Notifications\CheckoutAssetNotification;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use \Illuminate\Contracts\View\View; use \Illuminate\Contracts\View\View;
use League\Csv\Reader; use League\Csv\Reader;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -277,7 +275,7 @@ class ReportsController extends Controller
if ($actionlog->target) { if ($actionlog->target) {
if ($actionlog->targetType() == 'user') { if ($actionlog->targetType() == 'user') {
$target_name = $actionlog->target->getFullNameAttribute(); $target_name = $actionlog->target->display_name;
} else { } else {
$target_name = $actionlog->target->getDisplayNameAttribute(); $target_name = $actionlog->target->getDisplayNameAttribute();
} }
@@ -291,7 +289,7 @@ class ReportsController extends Controller
$row = [ $row = [
$actionlog->created_at, $actionlog->created_at,
($actionlog->adminuser) ? e($actionlog->adminuser->getFullNameAttribute()) : '', ($actionlog->adminuser) ? e($actionlog->adminuser->display_name) : '',
$actionlog->present()->actionType(), $actionlog->present()->actionType(),
e($actionlog->itemType()), e($actionlog->itemType()),
($actionlog->itemType() == 'user') ? $actionlog->filename : $item_name, ($actionlog->itemType() == 'user') ? $actionlog->filename : $item_name,
@@ -858,7 +856,7 @@ class ReportsController extends Controller
} }
if ($request->filled('assigned_to')) { if ($request->filled('assigned_to')) {
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? $asset->assigned->getFullNameAttribute() : ($asset->assigned ? $asset->assigned->display_name : ''); $row[] = ($asset->checkedOutToUser() && $asset->assigned) ?? $asset->assigned->display_name;
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? 'user' : $asset->assignedType(); $row[] = ($asset->checkedOutToUser() && $asset->assigned) ? 'user' : $asset->assignedType();
} }
@@ -1038,11 +1036,11 @@ class ReportsController extends Controller
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0 * @version v1.0
*/ */
public function getAssetMaintenancesReport() : View public function getMaintenancesReport() : View
{ {
$this->authorize('reports.view'); $this->authorize('reports.view');
return view('reports.asset_maintenances'); return view('reports.maintenances');
} }
/** /**
@@ -1051,11 +1049,11 @@ class ReportsController extends Controller
* @author Vincent Sposato <vincent.sposato@gmail.com> * @author Vincent Sposato <vincent.sposato@gmail.com>
* @version v1.0 * @version v1.0
*/ */
public function exportAssetMaintenancesReport() : Response public function exportMaintenancesReport() : Response
{ {
$this->authorize('reports.view'); $this->authorize('reports.view');
// Grab all the improvements // Grab all the improvements
$assetMaintenances = AssetMaintenance::with('asset', 'supplier') $Maintenances = Maintenance::with('asset', 'supplier')
->orderBy('created_at', 'DESC') ->orderBy('created_at', 'DESC')
->get(); ->get();
@@ -1063,36 +1061,36 @@ class ReportsController extends Controller
$header = [ $header = [
trans('admin/hardware/table.asset_tag'), trans('admin/hardware/table.asset_tag'),
trans('admin/asset_maintenances/table.asset_name'), trans('admin/maintenances/table.asset_name'),
trans('general.supplier'), trans('general.supplier'),
trans('admin/asset_maintenances/form.asset_maintenance_type'), trans('admin/maintenances/form.asset_maintenance_type'),
trans('admin/asset_maintenances/form.title'), trans('admin/maintenances/form.title'),
trans('admin/asset_maintenances/form.start_date'), trans('admin/maintenances/form.start_date'),
trans('admin/asset_maintenances/form.completion_date'), trans('admin/maintenances/form.completion_date'),
trans('admin/asset_maintenances/form.asset_maintenance_time'), trans('admin/maintenances/form.asset_maintenance_time'),
trans('admin/asset_maintenances/form.cost'), trans('admin/maintenances/form.cost'),
]; ];
$header = array_map('trim', $header); $header = array_map('trim', $header);
$rows[] = implode(',', $header); $rows[] = implode(',', $header);
foreach ($assetMaintenances as $assetMaintenance) { foreach ($Maintenances as $maintenance) {
$row = []; $row = [];
$row[] = str_replace(',', '', e($assetMaintenance->asset->asset_tag)); $row[] = str_replace(',', '', e($maintenance->asset->asset_tag));
$row[] = str_replace(',', '', e($assetMaintenance->asset->name)); $row[] = str_replace(',', '', e($maintenance->asset->name));
$row[] = str_replace(',', '', e($assetMaintenance->supplier->name)); $row[] = str_replace(',', '', e($maintenance->supplier->name));
$row[] = e($assetMaintenance->improvement_type); $row[] = e($maintenance->improvement_type);
$row[] = e($assetMaintenance->title); $row[] = e($maintenance->name);
$row[] = e($assetMaintenance->start_date); $row[] = e($maintenance->start_date);
$row[] = e($assetMaintenance->completion_date); $row[] = e($maintenance->completion_date);
if (is_null($assetMaintenance->asset_maintenance_time)) { if (is_null($maintenance->asset_maintenance_time)) {
$improvementTime = (int) Carbon::now() $improvementTime = (int) Carbon::now()
->diffInDays(Carbon::parse($assetMaintenance->start_date), true); ->diffInDays(Carbon::parse($maintenance->start_date), true);
} else { } else {
$improvementTime = (int) $assetMaintenance->asset_maintenance_time; $improvementTime = (int) $maintenance->asset_maintenance_time;
} }
$row[] = $improvementTime; $row[] = $improvementTime;
$row[] = trans('general.currency') . Helper::formatCurrencyOutput($assetMaintenance->cost); $row[] = trans('general.currency') . Helper::formatCurrencyOutput($maintenance->cost);
$rows[] = implode(',', $row); $rows[] = implode(',', $row);
} }

View File

@@ -873,6 +873,7 @@ class SettingsController extends Controller
$setting->ldap_default_group = $request->input('ldap_default_group'); $setting->ldap_default_group = $request->input('ldap_default_group');
$setting->ldap_filter = $request->input('ldap_filter'); $setting->ldap_filter = $request->input('ldap_filter');
$setting->ldap_username_field = $request->input('ldap_username_field'); $setting->ldap_username_field = $request->input('ldap_username_field');
$setting->ldap_display_name = $request->input('ldap_display_name');
$setting->ldap_lname_field = $request->input('ldap_lname_field'); $setting->ldap_lname_field = $request->input('ldap_lname_field');
$setting->ldap_fname_field = $request->input('ldap_fname_field'); $setting->ldap_fname_field = $request->input('ldap_fname_field');
$setting->ldap_auth_filter_query = $request->input('ldap_auth_filter_query'); $setting->ldap_auth_filter_query = $request->input('ldap_auth_filter_query');
@@ -889,7 +890,12 @@ class SettingsController extends Controller
$setting->ldap_pw_sync = $request->input('ldap_pw_sync', '0'); $setting->ldap_pw_sync = $request->input('ldap_pw_sync', '0');
$setting->custom_forgot_pass_url = $request->input('custom_forgot_pass_url'); $setting->custom_forgot_pass_url = $request->input('custom_forgot_pass_url');
$setting->ldap_phone_field = $request->input('ldap_phone'); $setting->ldap_phone_field = $request->input('ldap_phone');
$setting->ldap_mobile = $request->input('ldap_mobile');
$setting->ldap_jobtitle = $request->input('ldap_jobtitle'); $setting->ldap_jobtitle = $request->input('ldap_jobtitle');
$setting->ldap_address = $request->input('ldap_address');
$setting->ldap_city = $request->input('ldap_city');
$setting->ldap_state = $request->input('ldap_state');
$setting->ldap_zip = $request->input('ldap_zip');
$setting->ldap_country = $request->input('ldap_country'); $setting->ldap_country = $request->input('ldap_country');
$setting->ldap_location = $request->input('ldap_location'); $setting->ldap_location = $request->input('ldap_location');
$setting->ldap_dept = $request->input('ldap_dept'); $setting->ldap_dept = $request->input('ldap_dept');
@@ -1084,6 +1090,7 @@ class SettingsController extends Controller
if (! config('app.lock_passwords')) { if (! config('app.lock_passwords')) {
if (Storage::exists($path.'/'.$filename)) { if (Storage::exists($path.'/'.$filename)) {
Log::warning('User '.auth()->user()->username.' is attempting to download backup file: '.$filename);
return StorageHelper::downloader($path.'/'.$filename); return StorageHelper::downloader($path.'/'.$filename);
} else { } else {
// Redirect to the backup page // Redirect to the backup page
@@ -1111,6 +1118,7 @@ class SettingsController extends Controller
if (Storage::exists($path . '/' . $filename)) { if (Storage::exists($path . '/' . $filename)) {
try { try {
Log::warning('User '.auth()->user()->username.' is attempting to delete backup file: '.$filename);
Storage::delete($path . '/' . $filename); Storage::delete($path . '/' . $filename);
return redirect()->route('settings.backups.index')->with('success', trans('admin/settings/message.backup.file_deleted')); return redirect()->route('settings.backups.index')->with('success', trans('admin/settings/message.backup.file_deleted'));
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -1190,7 +1198,7 @@ class SettingsController extends Controller
'--force' => true, '--force' => true,
]); ]);
Log::debug('Attempting to restore from: '. storage_path($path).'/'.$filename); Log::warning('User '.auth()->user()->username.' is attempting to restore from: '. storage_path($path).'/'.$filename);
$restore_params = [ $restore_params = [
'--force' => true, '--force' => true,
@@ -1339,9 +1347,11 @@ class SettingsController extends Controller
'name' => config('mail.from.name'), 'name' => config('mail.from.name'),
'email' => config('mail.from.address'), 'email' => config('mail.from.address'),
])->notify(new MailTest()); ])->notify(new MailTest());
Log::debug('Attempting to send mail to '.config('mail.from.address'));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('mail_sent.mail_sent'))); return response()->json(Helper::formatStandardApiResponse('success', null, trans('mail_sent.mail_sent')));
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('Mail sent from '.config('mail.from.address') .' with errors '. $e->getMessage());
Log::debug($e);
return response()->json(Helper::formatStandardApiResponse('success', null, $e->getMessage())); return response()->json(Helper::formatStandardApiResponse('success', null, $e->getMessage()));
} }
} }

View File

@@ -4,7 +4,6 @@ namespace App\Http\Controllers;
use App\Http\Requests\ImageUploadRequest; use App\Http\Requests\ImageUploadRequest;
use App\Models\Supplier; use App\Models\Supplier;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View; use \Illuminate\Contracts\View\View;
@@ -122,7 +121,7 @@ class SuppliersController extends Controller
public function destroy($supplierId) : RedirectResponse public function destroy($supplierId) : RedirectResponse
{ {
$this->authorize('delete', Supplier::class); $this->authorize('delete', Supplier::class);
if (is_null($supplier = Supplier::with('asset_maintenances', 'assets', 'licenses')->withCount('asset_maintenances as asset_maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->find($supplierId))) { if (is_null($supplier = Supplier::with('maintenances', 'assets', 'licenses')->withCount('maintenances as maintenances_count', 'assets as assets_count', 'licenses as licenses_count')->find($supplierId))) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.not_found')); return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.not_found'));
} }
@@ -130,8 +129,8 @@ class SuppliersController extends Controller
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count])); return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_assets', ['asset_count' => (int) $supplier->assets_count]));
} }
if ($supplier->asset_maintenances_count > 0) { if ($supplier->maintenances_count > 0) {
return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_maintenances', ['asset_maintenances_count' => $supplier->asset_maintenances_count])); return redirect()->route('suppliers.index')->with('error', trans('admin/suppliers/message.delete.assoc_maintenances', ['maintenances_count' => $supplier->maintenances_count]));
} }
if ($supplier->licenses_count > 0) { if ($supplier->licenses_count > 0) {

View File

@@ -0,0 +1,162 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\StorageHelper;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* This controller provide the health route for
* the Snipe-IT Asset Management application.
*
* @version v1.0
*
* @return \Illuminate\Http\JsonResponse
*/
class UploadedFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to store so we can check permisisons
* @since [v8.2.2]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function store(UploadFileRequest $request, $object_type, $id) : RedirectResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('update', $object);
if (!$object) {
return redirect()->back()->withFragment('files')->with('error',trans('general.file_upload_status.invalid_object'));
}
// If the file storage directory doesn't exist, create it
if (! Storage::exists(self::$map_storage_path[$object_type])) {
Storage::makeDirectory(self::$map_storage_path[$object_type], 775);
}
if ($request->hasFile('file')) {
// Loop over the attached files and add them to the object
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile(self::$map_storage_path[$object_type], self::$map_file_prefix[$object_type].'-'.$object->id, $file);
$files[] = $file_name;
$object->logUpload($file_name, $request->get('notes'));
}
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')
->where('item_type', '=', self::$map_object_type[$object_type])
->where('item_id', '=', $id)->whereIn('filename', $files)
->get();
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.upload.success', count($files)));
}
// No files were submitted
return redirect()->back()->withFragment('files')->with('error', trans('general.file_upload_status.nofiles'));
}
/**
* Check for permissions and display the file.
* This isn't currently used, but is here for future use.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to delete from so we can check permisisons
* @param $file_id the ID of the file to show from the action_logs table
* @since [v8.2.2]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function show($object_type, $id, $file_id) : RedirectResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return redirect()->back()->withFragment('files')->with('error',trans('general.file_upload_status.invalid_object'));
}
// Check that the file being requested exists for the object
if (! $log = Actionlog::whereNotNull('filename')->where('item_type', self::$map_object_type[$object_type])->where('item_id', $object->id)->find($file_id))
{
return redirect()->back()->withFragment('files')->with('error', trans('general.file_upload_status.invalid_id'));
}
if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename))
{
return redirect()->back()->withFragment('files')->with('error', trans('general.file_upload_status.file_not_found'));
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download(self::$map_storage_path[$object_type].'/'.$log->filename, $log->filename, $headers);
}
return StorageHelper::downloader(self::$map_storage_path[$object_type].'/'.$log->filename);
}
/**
* Delete the associated file
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param string $object_type the type of object to upload the file to
* @param int $id the ID of the object to delete from so we can check permisisons
* @param $file_id the ID of the file to delete from the action_logs table
* @since [v8.2.2]
* @author [A. Gianotto <snipe@snipe.net>]
*/
public function destroy($object_type, $id, $file_id) : RedirectResponse
{
// Check the permissions to make sure the user can view the object
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) {
return redirect()->back()->withFragment('files')->with('error',trans('general.file_upload_status.invalid_object'));
}
// Check for the file
$log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)->first();
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
}
// Delete the record of the file
if ($log->delete()) {
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.delete.success', 1));
}
}
// The file doesn't seem to really exist, so report an error
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.delete.error', 1));
}
}

View File

@@ -1,129 +0,0 @@
<?php
namespace App\Http\Controllers\Users;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Models\Actionlog;
use App\Models\User;
use Symfony\Component\HttpFoundation\JsonResponse;
use Illuminate\Support\Facades\Storage;
class UserFilesController extends Controller
{
/**
* Return JSON response with a list of user details for the getIndex() view.
*
* @param UploadFileRequest $request
* @param int $userId
* @return string JSON
* @throws \Illuminate\Auth\Access\AuthorizationException
*@author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.6]
*/
public function store(UploadFileRequest $request, User $user)
{
$this->authorize('update', $user);
$files = $request->file('file');
if (is_null($files)) {
return redirect()->back()->with('error', trans('admin/users/message.upload.nofiles'));
}
foreach ($files as $file) {
$file_name = $request->handleFile('private_uploads/users/', 'user-'.$user->id, $file);
//Log the uploaded file to the log
$logAction = new Actionlog();
$logAction->item_id = $user->id;
$logAction->item_type = User::class;
$logAction->created_by = auth()->id();
$logAction->note = $request->input('notes');
$logAction->target_id = null;
$logAction->created_at = date("Y-m-d H:i:s");
$logAction->filename = $file_name;
$logAction->action_type = 'uploaded';
if (! $logAction->save()) {
return JsonResponse::create(['error' => 'Failed validation: '.print_r($logAction->getErrors(), true)], 500);
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.upload.success'));
}
}
/**
* Delete file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.6]
* @param int $userId
* @param int $fileId
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy($userId = null, $fileId = null)
{
if ($user = User::find($userId)) {
$this->authorize('delete', $user);
$rel_path = 'private_uploads/users';
if ($log = Actionlog::find($fileId)) {
$filename = $log->filename;
$log->delete();
if (Storage::exists($rel_path.'/'.$filename)) {
Storage::delete($rel_path.'/'.$filename);
return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.deletefile.success'));
}
}
// The log record doesn't exist somehow
return redirect()->back()->with('success', trans('admin/users/message.deletefile.success'));
}
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', ['id' => $userId]));
}
/**
* Display/download the uploaded file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.6]
* @param int $userId
* @param int $fileId
* @return mixed
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function show(User $user, $fileId = null)
{
if (empty($fileId)) {
return redirect()->route('users.show')->with('error', 'Invalid file request');
}
$this->authorize('view', $user);
if ($log = Actionlog::whereNotNull('filename')->where('item_id', $user->id)->find($fileId)) {
$file = 'private_uploads/users/'.$log->filename;
try {
return StorageHelper::showOrDownloadFile($file, $log->filename);
} catch (\Exception $e) {
return redirect()->route('users.show', ['user' => $user])->with('error', trans('general.file_not_found'));
}
}
// The log record doesn't exist somehow
return redirect()->route('users.show', ['user' => $user])->with('error', trans('general.log_record_not_found'));
}
}

View File

@@ -14,14 +14,9 @@ use App\Models\Group;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User; use App\Models\User;
use App\Notifications\WelcomeNotification; use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Storage;
use Redirect;
use Str;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Notifications\CurrentInventory; use App\Notifications\CurrentInventory;
@@ -95,6 +90,7 @@ class UsersController extends Controller
//Username, email, and password need to be handled specially because the need to respect config values on an edit. //Username, email, and password need to be handled specially because the need to respect config values on an edit.
$user->email = trim($request->input('email')); $user->email = trim($request->input('email'));
$user->username = trim($request->input('username')); $user->username = trim($request->input('username'));
$user->display_name = $request->input('display_name');
if ($request->filled('password')) { if ($request->filled('password')) {
$user->password = bcrypt($request->input('password')); $user->password = bcrypt($request->input('password'));
} }
@@ -105,6 +101,7 @@ class UsersController extends Controller
$user->activated = $request->input('activated', 0); $user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle'); $user->jobtitle = $request->input('jobtitle');
$user->phone = $request->input('phone'); $user->phone = $request->input('phone');
$user->mobile = $request->input('mobile');
$user->location_id = $request->input('location_id', null); $user->location_id = $request->input('location_id', null);
$user->department_id = $request->input('department_id', null); $user->department_id = $request->input('department_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null)); $user->company_id = Company::getIdForUser($request->input('company_id', null));
@@ -130,30 +127,35 @@ class UsersController extends Controller
} }
$user->permissions = json_encode($permissions_array); $user->permissions = json_encode($permissions_array);
// we have to invoke the // we have to invoke the form request here to handle image uploads
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar'); app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
session()->put(['redirect_option' => $request->get('redirect_option')]); if ($request->get('redirect_option') === 'back'){
session()->put(['redirect_option' => 'index']);
} else {
session()->put(['redirect_option' => $request->get('redirect_option')]);
}
if ($user->save()) { if ($user->save()) {
if (($user->activated == '1') && ($user->email != '') && ($request->input('send_welcome') == '1')) {
try {
$user->notify(new WelcomeNotification($user));
} catch (\Exception $e) {
Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
}
}
if ($request->filled('groups')) { if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups')); $user->groups()->sync($request->input('groups'));
} else { } else {
$user->groups()->sync([]); $user->groups()->sync([]);
} }
if (($request->input('email_user') == 1) && ($request->filled('email'))) {
// Send the credentials through email
$data = [];
$data['email'] = e($request->input('email'));
$data['username'] = e($request->input('username'));
$data['first_name'] = e($request->input('first_name'));
$data['last_name'] = e($request->input('last_name'));
$data['password'] = e($request->input('password'));
$user->notify(new WelcomeNotification($data));
}
return Helper::getRedirectOption($request, $user->id, 'Users') return Helper::getRedirectOption($request, $user->id, 'Users')
->with('success', trans('admin/users/message.success.create')); ->with('success', trans('admin/users/message.success.create'));
} }
@@ -248,22 +250,18 @@ class UsersController extends Controller
} }
} }
// Only save groups if the user is a superuser
if (auth()->user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
// Update the user fields // Update the user fields
$user->username = trim($request->input('username'));
$user->email = trim($request->input('email'));
$user->first_name = $request->input('first_name'); $user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name'); $user->last_name = $request->input('last_name');
$user->display_name = $request->input('display_name');
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0; $user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale'); $user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num'); $user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle', null); $user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone'); $user->phone = $request->input('phone');
$user->mobile = $request->input('mobile');
$user->location_id = $request->input('location_id', null); $user->location_id = $request->input('location_id', null);
$user->company_id = Company::getIdForUser($request->input('company_id', null)); $user->company_id = Company::getIdForUser($request->input('company_id', null));
$user->manager_id = $request->input('manager_id', null); $user->manager_id = $request->input('manager_id', null);
@@ -273,8 +271,6 @@ class UsersController extends Controller
$user->city = $request->input('city', null); $user->city = $request->input('city', null);
$user->state = $request->input('state', null); $user->state = $request->input('state', null);
$user->country = $request->input('country', null); $user->country = $request->input('country', null);
// if a user is editing themselves we should always keep activated true
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
$user->zip = $request->input('zip', null); $user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0); $user->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0); $user->vip = $request->input('vip', 0);
@@ -283,30 +279,49 @@ class UsersController extends Controller
$user->end_date = $request->input('end_date', null); $user->end_date = $request->input('end_date', null);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0); $user->autoassign_licenses = $request->input('autoassign_licenses', 0);
// Set this here so that we can overwrite it later if the user is an admin or superadmin
$user->activated = $request->input('activated', auth()->user()->is($user) ? 1 : $user->activated);
// Update the location of any assets checked out to this user // Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class) Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id) ->where('assigned_to', $user->id)
->update(['location_id' => $request->input('location_id', null)]); ->update(['location_id' => $request->input('location_id', null)]);
// Do we want to update the user password? // check for permissions related fields and only set them if the user has permission to edit them
if ($request->filled('password')) { if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
$user->password = bcrypt($request->input('password'));
$user->username = trim($request->input('username'));
$user->email = trim($request->input('email'));
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
// Do we want to update the user password?
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
$permissions_array = $request->input('permission');
// Strip out the superuser permission if the user isn't a superadmin
if (! auth()->user()->isSuperUser()) {
unset($permissions_array['superuser']);
$permissions_array['superuser'] = $orig_superuser;
}
$user->permissions = json_encode($permissions_array);
// Only save groups if the user is a superuser
if (auth()->user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
} }
// Update the location of any assets checked out to this user // Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class) Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id) ->where('assigned_to', $user->id)
->update(['location_id' => $user->location_id]); ->update(['location_id' => $user->location_id]);
$permissions_array = $request->input('permission');
// Strip out the superuser permission if the user isn't a superadmin
if (! auth()->user()->isSuperUser()) {
unset($permissions_array['superuser']);
$permissions_array['superuser'] = $orig_superuser;
}
$user->permissions = json_encode($permissions_array);
// Handle uploaded avatar // Handle uploaded avatar
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar'); app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
@@ -440,7 +455,7 @@ class UsersController extends Controller
app('request')->request->set('permissions', $permissions); app('request')->request->set('permissions', $permissions);
$user_to_clone = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($user->id); $user_to_clone = User::with('userloc')->withTrashed()->find($user->id);
// Make sure they can view this particular user // Make sure they can view this particular user
$this->authorize('view', $user_to_clone); $this->authorize('view', $user_to_clone);
@@ -455,6 +470,8 @@ class UsersController extends Controller
$user->last_name = ''; $user->last_name = '';
$user->email = substr($user->email, ($pos = strpos($user->email, '@')) !== false ? $pos : 0); $user->email = substr($user->email, ($pos = strpos($user->email, '@')) !== false ? $pos : 0);
$user->id = null; $user->id = null;
$user->username = null;
$user->avatar = null;
// Get this user's groups // Get this user's groups
$userGroups = $user_to_clone->groups()->pluck('name', 'id'); $userGroups = $user_to_clone->groups()->pluck('name', 'id');
@@ -470,7 +487,7 @@ class UsersController extends Controller
->with('user', $user) ->with('user', $user)
->with('groups', Group::pluck('name', 'id')) ->with('groups', Group::pluck('name', 'id'))
->with('userGroups', $userGroups) ->with('userGroups', $userGroups)
->with('clone_user', $user_to_clone) ->with('cloned_model', $user_to_clone)
->with('item', $user); ->with('item', $user);
} }
@@ -561,10 +578,10 @@ class UsersController extends Controller
$user->employee_num, $user->employee_num,
$user->first_name, $user->first_name,
$user->last_name, $user->last_name,
$user->present()->fullName(), $user->display_name,
$user->username, $user->username,
$user->email, $user->email,
($user->manager) ? $user->manager->present()->fullName() : '', ($user->manager) ? $user->manager->display_name : '',
($user->userloc) ? $user->userloc->name : '', ($user->userloc) ? $user->userloc->name : '',
($user->department) ? $user->department->name : '', ($user->department) ? $user->department->name : '',
$user->assets->count(), $user->assets->count(),

View File

@@ -185,7 +185,7 @@ class ViewAssetsController extends Controller
$logaction->target_type = User::class; $logaction->target_type = User::class;
$data['item_quantity'] = $request->has('request-quantity') ? e($request->input('request-quantity')) : 1; $data['item_quantity'] = $request->has('request-quantity') ? e($request->input('request-quantity')) : 1;
$data['requested_by'] = $user->present()->fullName(); $data['requested_by'] = $user->display_name;
$data['item'] = $item; $data['item'] = $item;
$data['item_type'] = $itemType; $data['item_type'] = $itemType;
$data['target'] = auth()->user(); $data['target'] = auth()->user();

View File

@@ -26,7 +26,6 @@ class SecurityHeaders
$response = $next($request); $response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Ugh. Feature-Policy is dumb and clumsy and mostly irrelevant for Snipe-IT, // Ugh. Feature-Policy is dumb and clumsy and mostly irrelevant for Snipe-IT,
// since we don't provide any way to IFRAME anything in in the first place. // since we don't provide any way to IFRAME anything in in the first place.

View File

@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Storage;
use Intervention\Image\Exception\NotReadableException; use Intervention\Image\Exception\NotReadableException;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class ImageUploadRequest extends Request class ImageUploadRequest extends Request
{ {
@@ -70,19 +71,25 @@ class ImageUploadRequest extends Request
public function handleImages($item, $w = 600, $form_fieldname = 'image', $path = null, $db_fieldname = 'image') public function handleImages($item, $w = 600, $form_fieldname = 'image', $path = null, $db_fieldname = 'image')
{ {
$type = strtolower(class_basename(get_class($item))); $type = class_basename(get_class($item));
if (is_null($path)) { if (is_null($path)) {
$path = str_plural($type); $path = strtolower(str_plural($type));
if ($type == 'assetmodel') { if ($type == 'AssetModel') {
$path = 'models'; $path = 'models';
} }
if ($type == 'user') { if ($type == 'user') {
$path = 'avatars'; $path = 'avatars';
} }
}
if (!Storage::disk('public')->exists($path)) {
Storage::disk('public')->makeDirectory($path);
} }
if ($this->offsetGet($form_fieldname) instanceof UploadedFile) { if ($this->offsetGet($form_fieldname) instanceof UploadedFile) {
@@ -93,10 +100,9 @@ class ImageUploadRequest extends Request
if (isset($image)) { if (isset($image)) {
if (!config('app.lock_passwords')) {
$ext = $image->guessExtension(); $ext = $image->guessExtension();
$file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext; $file_name = $type.'-'.$form_fieldname.($item->id ?? '-'.$item->id).'-'.str_random(10).'.'.$ext;
if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) { if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) {
// If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing // If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing
@@ -138,7 +144,7 @@ class ImageUploadRequest extends Request
// Remove Current image if exists // Remove Current image if exists
$item = $this->deleteExistingImage($item, $path, $db_fieldname); $item = $this->deleteExistingImage($item, $path, $db_fieldname);
$item->{$db_fieldname} = $file_name; $item->{$db_fieldname} = $file_name;
}
// If the user isn't uploading anything new but wants to delete their old image, do so // If the user isn't uploading anything new but wants to delete their old image, do so

View File

@@ -109,7 +109,7 @@ class SettingsSamlRequest extends FormRequest
]; ];
$pkey = openssl_pkey_new([ $pkey = openssl_pkey_new([
'private_key_bits' => 2048, 'private_key_bits' => config('app.saml_key_size'),
'private_key_type' => OPENSSL_KEYTYPE_RSA, 'private_key_type' => OPENSSL_KEYTYPE_RSA,
]); ]);

View File

@@ -80,9 +80,20 @@ class UploadFileRequest extends Request
{ {
$attributes = []; $attributes = [];
if ($this->file) { if (($this->file) && (is_array($this->file))) {
for ($i = 0; $i < count($this->file); $i++) { for ($i = 0; $i < count($this->file); $i++) {
$attributes['file.'.$i] = $this->file[$i]->getClientOriginalName();
try {
if ($this->file[$i]) {
$attributes['file.'.$i] = $this->file[$i]->getClientOriginalName();
}
} catch (\Exception $e) {
$attributes['file.'.$i] = 'Invalid file';
}
} }
} }

View File

@@ -44,7 +44,7 @@ class AccessoriesTransformer
'checkouts_count' => $accessory->checkouts_count, 'checkouts_count' => $accessory->checkouts_count,
'created_by' => ($accessory->adminuser) ? [ 'created_by' => ($accessory->adminuser) ? [
'id' => (int) $accessory->adminuser->id, 'id' => (int) $accessory->adminuser->id,
'name'=> e($accessory->adminuser->present()->fullName()), 'name'=> e($accessory->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($accessory->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($accessory->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($accessory->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($accessory->updated_at, 'datetime'),

View File

@@ -2,6 +2,7 @@
namespace App\Http\Transformers; namespace App\Http\Transformers;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Models\Actionlog; use App\Models\Actionlog;
use App\Models\Asset; use App\Models\Asset;
use App\Models\CustomField; use App\Models\CustomField;
@@ -16,6 +17,7 @@ use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class ActionlogsTransformer class ActionlogsTransformer
{ {
@@ -133,24 +135,6 @@ class ActionlogsTransformer
$clean_meta= $this->changedInfo($clean_meta); $clean_meta= $this->changedInfo($clean_meta);
} }
$file_url = '';
if($actionlog->filename!='') {
if ($actionlog->action_type == 'accepted') {
$file_url = route('log.storedeula.download', ['filename' => $actionlog->filename]);
} else {
if ($actionlog->item) {
if ($actionlog->itemType() == 'asset') {
$file_url = route('show/assetfile', ['asset' => $actionlog->item->id, 'fileId' => $actionlog->id]);
} elseif ($actionlog->itemType() == 'accessory') {
$file_url = route('show.accessoryfile', ['accessoryId' => $actionlog->item->id, 'fileId' => $actionlog->id]);
} elseif ($actionlog->itemType() == 'license') {
$file_url = route('show.licensefile', ['licenseId' => $actionlog->item->id, 'fileId' => $actionlog->id]);
} elseif ($actionlog->itemType() == 'user') {
$file_url = route('show/userfile', ['user' => $actionlog->item->id, 'fileId' => $actionlog->id]);
}
}
}
}
$array = [ $array = [
'id' => (int) $actionlog->id, 'id' => (int) $actionlog->id,
@@ -158,13 +142,15 @@ class ActionlogsTransformer
'file' => ($actionlog->filename!='') 'file' => ($actionlog->filename!='')
? ?
[ [
'url' => $file_url, 'url' => $actionlog->uploads_file_url(),
'filename' => $actionlog->filename, 'filename' => $actionlog->filename,
'inlineable' => StorageHelper::allowSafeInline($actionlog->uploads_file_url()),
'exists_on_disk' => Storage::exists($actionlog->uploads_file_path()) ? true : false,
] : null, ] : null,
'item' => ($actionlog->item) ? [ 'item' => ($actionlog->item) ? [
'id' => (int) $actionlog->item->id, 'id' => (int) $actionlog->item->id,
'name' => ($actionlog->itemType()=='user') ? e($actionlog->item->getFullNameAttribute()) : e($actionlog->item->getDisplayNameAttribute()), 'name' => e($actionlog->item->display_name) ?? null,
'type' => e($actionlog->itemType()), 'type' => e($actionlog->itemType()),
'serial' =>e($actionlog->item->serial) ? e($actionlog->item->serial) : null 'serial' =>e($actionlog->item->serial) ? e($actionlog->item->serial) : null
] : null, ] : null,
@@ -179,19 +165,19 @@ class ActionlogsTransformer
'action_type' => $actionlog->present()->actionType(), 'action_type' => $actionlog->present()->actionType(),
'admin' => ($actionlog->adminuser) ? [ 'admin' => ($actionlog->adminuser) ? [
'id' => (int) $actionlog->adminuser->id, 'id' => (int) $actionlog->adminuser->id,
'name' => e($actionlog->adminuser->getFullNameAttribute()), 'name' => e($actionlog->adminuser->display_name),
'first_name'=> e($actionlog->adminuser->first_name), 'first_name'=> e($actionlog->adminuser->first_name),
'last_name'=> e($actionlog->adminuser->last_name) 'last_name'=> e($actionlog->adminuser->last_name)
] : null, ] : null,
'created_by' => ($actionlog->adminuser) ? [ 'created_by' => ($actionlog->adminuser) ? [
'id' => (int) $actionlog->adminuser->id, 'id' => (int) $actionlog->adminuser->id,
'name' => e($actionlog->adminuser->getFullNameAttribute()), 'name' => e($actionlog->adminuser->display_name),
'first_name'=> e($actionlog->adminuser->first_name), 'first_name'=> e($actionlog->adminuser->first_name),
'last_name'=> e($actionlog->adminuser->last_name) 'last_name'=> e($actionlog->adminuser->last_name)
] : null, ] : null,
'target' => ($actionlog->target) ? [ 'target' => ($actionlog->target) ? [
'id' => (int) $actionlog->target->id, 'id' => (int) $actionlog->target->id,
'name' => ($actionlog->targetType()=='user') ? e($actionlog->target->getFullNameAttribute()) : e($actionlog->target->getDisplayNameAttribute()), 'name' => ($actionlog->target->display_name) ?? null,
'type' => e($actionlog->targetType()), 'type' => e($actionlog->targetType()),
] : null, ] : null,

View File

@@ -68,7 +68,7 @@ class AssetModelsTransformer
'notes' => Helper::parseEscapedMarkedownInline($assetmodel->notes), 'notes' => Helper::parseEscapedMarkedownInline($assetmodel->notes),
'created_by' => ($assetmodel->adminuser) ? [ 'created_by' => ($assetmodel->adminuser) ? [
'id' => (int) $assetmodel->adminuser->id, 'id' => (int) $assetmodel->adminuser->id,
'name'=> e($assetmodel->adminuser->present()->fullName()), 'name'=> e($assetmodel->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($assetmodel->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($assetmodel->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmodel->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($assetmodel->updated_at, 'datetime'),

View File

@@ -58,6 +58,13 @@ class AssetsTransformer
'id' => (int) $asset->model->manufacturer->id, 'id' => (int) $asset->model->manufacturer->id,
'name'=> e($asset->model->manufacturer->name), 'name'=> e($asset->model->manufacturer->name),
] : null, ] : null,
'depreciation' => (($asset->model) && ($asset->model->depreciation)) ? [
'id' => (int) $asset->model->depreciation->id,
'name'=> e($asset->model->depreciation->name),
'months'=> (int) $asset->model->depreciation->months,
'type'=> e($asset->model->depreciation->depreciation_type),
'minimum'=> ($asset->model->depreciation->depreciation_min) ? (int) $asset->model->depreciation->depreciation_min : null,
] : null,
'supplier' => ($asset->supplier) ? [ 'supplier' => ($asset->supplier) ? [
'id' => (int) $asset->supplier->id, 'id' => (int) $asset->supplier->id,
'name'=> e($asset->supplier->name), 'name'=> e($asset->supplier->name),
@@ -80,12 +87,11 @@ class AssetsTransformer
'qr' => ($setting->qr_code=='1') ? config('app.url').'/uploads/barcodes/qr-'.str_slug($asset->asset_tag).'-'.str_slug($asset->id).'.png' : null, 'qr' => ($setting->qr_code=='1') ? config('app.url').'/uploads/barcodes/qr-'.str_slug($asset->asset_tag).'-'.str_slug($asset->id).'.png' : null,
'alt_barcode' => ($setting->alt_barcode_enabled=='1') ? config('app.url').'/uploads/barcodes/'.str_slug($setting->alt_barcode).'-'.str_slug($asset->asset_tag).'.png' : null, 'alt_barcode' => ($setting->alt_barcode_enabled=='1') ? config('app.url').'/uploads/barcodes/'.str_slug($setting->alt_barcode).'-'.str_slug($asset->asset_tag).'.png' : null,
'assigned_to' => $this->transformAssignedTo($asset), 'assigned_to' => $this->transformAssignedTo($asset),
'jobtitle' => $asset->assigned ? e($asset->assigned->jobtitle) : null,
'warranty_months' => ($asset->warranty_months > 0) ? e($asset->warranty_months.' '.trans('admin/hardware/form.months')) : null, 'warranty_months' => ($asset->warranty_months > 0) ? e($asset->warranty_months.' '.trans('admin/hardware/form.months')) : null,
'warranty_expires' => ($asset->warranty_months > 0) ? Helper::getFormattedDateObject($asset->warranty_expires, 'date') : null, 'warranty_expires' => ($asset->warranty_months > 0) ? Helper::getFormattedDateObject($asset->warranty_expires, 'date') : null,
'created_by' => ($asset->adminuser) ? [ 'created_by' => ($asset->adminuser) ? [
'id' => (int) $asset->adminuser->id, 'id' => (int) $asset->adminuser->id,
'name'=> e($asset->adminuser->present()->fullName()), 'name'=> e($asset->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($asset->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($asset->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($asset->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($asset->updated_at, 'datetime'),
@@ -204,6 +210,7 @@ class AssetsTransformer
'last_name'=> ($asset->assigned->last_name) ? e($asset->assigned->last_name) : null, 'last_name'=> ($asset->assigned->last_name) ? e($asset->assigned->last_name) : null,
'email'=> ($asset->assigned->email) ? e($asset->assigned->email) : null, 'email'=> ($asset->assigned->email) ? e($asset->assigned->email) : null,
'employee_number' => ($asset->assigned->employee_num) ? e($asset->assigned->employee_num) : null, 'employee_number' => ($asset->assigned->employee_num) ? e($asset->assigned->employee_num) : null,
'jobtitle' => $asset->assigned->jobtitle ? e($asset->assigned->jobtitle) : null,
'type' => 'user', 'type' => 'user',
] : null; ] : null;
} }
@@ -280,7 +287,7 @@ class AssetsTransformer
'id' => (int) $asset->id, 'id' => (int) $asset->id,
'image' => ($asset->getImageUrl()) ? $asset->getImageUrl() : null, 'image' => ($asset->getImageUrl()) ? $asset->getImageUrl() : null,
'type' => 'asset', 'type' => 'asset',
'name' => e($asset->present()->fullName()), 'name' => e($asset->display_name),
'model' => ($asset->model) ? e($asset->model->name) : null, 'model' => ($asset->model) ? e($asset->model->name) : null,
'model_number' => (($asset->model) && ($asset->model->model_number)) ? e($asset->model->model_number) : null, 'model_number' => (($asset->model) && ($asset->model->model_number)) ? e($asset->model->model_number) : null,
'asset_tag' => e($asset->asset_tag), 'asset_tag' => e($asset->asset_tag),

View File

@@ -64,7 +64,7 @@ class CategoriesTransformer
'licenses_count' => (int) $category->licenses_count, 'licenses_count' => (int) $category->licenses_count,
'created_by' => ($category->adminuser) ? [ 'created_by' => ($category->adminuser) ? [
'id' => (int) $category->adminuser->id, 'id' => (int) $category->adminuser->id,
'name'=> e($category->adminuser->present()->fullName()), 'name'=> e($category->adminuser->display_name),
] : null, ] : null,
'notes' => Helper::parseEscapedMarkedownInline($category->notes), 'notes' => Helper::parseEscapedMarkedownInline($category->notes),
'created_at' => Helper::getFormattedDateObject($category->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($category->created_at, 'datetime'),

View File

@@ -38,7 +38,7 @@ class CompaniesTransformer
'users_count' => (int) $company->users_count, 'users_count' => (int) $company->users_count,
'created_by' => ($company->adminuser) ? [ 'created_by' => ($company->adminuser) ? [
'id' => (int) $company->adminuser->id, 'id' => (int) $company->adminuser->id,
'name'=> e($company->adminuser->present()->fullName()), 'name'=> e($company->adminuser->display_name),
] : null, ] : null,
'notes' => Helper::parseEscapedMarkedownInline($company->notes), 'notes' => Helper::parseEscapedMarkedownInline($company->notes),
'created_at' => Helper::getFormattedDateObject($company->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($company->created_at, 'datetime'),

View File

@@ -51,7 +51,7 @@ class ComponentsTransformer
'notes' => ($component->notes) ? Helper::parseEscapedMarkedownInline($component->notes) : null, 'notes' => ($component->notes) ? Helper::parseEscapedMarkedownInline($component->notes) : null,
'created_by' => ($component->adminuser) ? [ 'created_by' => ($component->adminuser) ? [
'id' => (int) $component->adminuser->id, 'id' => (int) $component->adminuser->id,
'name'=> e($component->adminuser->present()->fullName()), 'name'=> e($component->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($component->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($component->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($component->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($component->updated_at, 'datetime'),

View File

@@ -25,7 +25,7 @@ class ConsumablesTransformer
$array = [ $array = [
'id' => (int) $consumable->id, 'id' => (int) $consumable->id,
'name' => e($consumable->name), 'name' => e($consumable->name),
'image' => ($consumable->image) ? Storage::disk('public')->url('consumables/'.e($consumable->image)) : null, 'image' => ($consumable->getImageUrl()) ? ($consumable->getImageUrl()) : null,
'category' => ($consumable->category) ? ['id' => $consumable->category->id, 'name' => e($consumable->category->name)] : null, 'category' => ($consumable->category) ? ['id' => $consumable->category->id, 'name' => e($consumable->category->name)] : null,
'company' => ($consumable->company) ? ['id' => (int) $consumable->company->id, 'name' => e($consumable->company->name)] : null, 'company' => ($consumable->company) ? ['id' => (int) $consumable->company->id, 'name' => e($consumable->company->name)] : null,
'item_no' => e($consumable->item_no), 'item_no' => e($consumable->item_no),
@@ -42,7 +42,7 @@ class ConsumablesTransformer
'notes' => ($consumable->notes) ? Helper::parseEscapedMarkedownInline($consumable->notes) : null, 'notes' => ($consumable->notes) ? Helper::parseEscapedMarkedownInline($consumable->notes) : null,
'created_by' => ($consumable->adminuser) ? [ 'created_by' => ($consumable->adminuser) ? [
'id' => (int) $consumable->adminuser->id, 'id' => (int) $consumable->adminuser->id,
'name'=> e($consumable->adminuser->present()->fullName()), 'name'=> e($consumable->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($consumable->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($consumable->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($consumable->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($consumable->updated_at, 'datetime'),

View File

@@ -35,7 +35,7 @@ class DepartmentsTransformer
] : null, ] : null,
'manager' => ($department->manager) ? [ 'manager' => ($department->manager) ? [
'id' => (int) $department->manager->id, 'id' => (int) $department->manager->id,
'name' => e($department->manager->getFullNameAttribute()), 'name' => e($department->manager->display_name),
'first_name'=> e($department->manager->first_name), 'first_name'=> e($department->manager->first_name),
'last_name'=> e($department->manager->last_name), 'last_name'=> e($department->manager->last_name),
] : null, ] : null,

View File

@@ -33,7 +33,7 @@ class DepreciationsTransformer
'licenses_count' => ($depreciation->licenses_count > 0) ? (int) $depreciation->licenses_count : 0, 'licenses_count' => ($depreciation->licenses_count > 0) ? (int) $depreciation->licenses_count : 0,
'created_by' => ($depreciation->adminuser) ? [ 'created_by' => ($depreciation->adminuser) ? [
'id' => (int) $depreciation->adminuser->id, 'id' => (int) $depreciation->adminuser->id,
'name'=> e($depreciation->adminuser->present()->fullName()), 'name'=> e($depreciation->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($depreciation->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($depreciation->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($depreciation->updated_at, 'datetime') 'updated_at' => Helper::getFormattedDateObject($depreciation->updated_at, 'datetime')

View File

@@ -29,7 +29,7 @@ class GroupsTransformer
'notes' => Helper::parseEscapedMarkedownInline($group->notes), 'notes' => Helper::parseEscapedMarkedownInline($group->notes),
'created_by' => ($group->adminuser) ? [ 'created_by' => ($group->adminuser) ? [
'id' => (int) $group->adminuser->id, 'id' => (int) $group->adminuser->id,
'name'=> e($group->adminuser->present()->fullName()), 'name'=> e($group->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($group->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($group->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($group->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($group->updated_at, 'datetime'),

View File

@@ -48,7 +48,7 @@ class LicensesTransformer
'category' => ($license->category) ? ['id' => (int) $license->category->id, 'name'=> e($license->category->name)] : null, 'category' => ($license->category) ? ['id' => (int) $license->category->id, 'name'=> e($license->category->name)] : null,
'created_by' => ($license->adminuser) ? [ 'created_by' => ($license->adminuser) ? [
'id' => (int) $license->adminuser->id, 'id' => (int) $license->adminuser->id,
'name'=> e($license->adminuser->present()->fullName()), 'name'=> e($license->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($license->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($license->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($license->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($license->updated_at, 'datetime'),
@@ -62,7 +62,7 @@ class LicensesTransformer
'checkin' => Gate::allows('checkin', License::class), 'checkin' => Gate::allows('checkin', License::class),
'clone' => Gate::allows('create', License::class), 'clone' => Gate::allows('create', License::class),
'update' => Gate::allows('update', License::class), 'update' => Gate::allows('update', License::class),
'delete' => (Gate::allows('delete', License::class) && ($license->free_seats_count > 0)) ? true : false, 'delete' => (Gate::allows('delete', License::class) && ($license->free_seats_count == $license->seats)) ? true : false,
]; ];
$array += $permissions_array; $array += $permissions_array;

View File

@@ -4,23 +4,24 @@ namespace App\Http\Transformers;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Models\Asset; use App\Models\Asset;
use App\Models\AssetMaintenance; use App\Models\Maintenance;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Storage;
class AssetMaintenancesTransformer class MaintenancesTransformer
{ {
public function transformAssetMaintenances(Collection $assetmaintenances, $total) public function transformMaintenances(Collection $maintenances, $total)
{ {
$array = []; $array = [];
foreach ($assetmaintenances as $assetmaintenance) { foreach ($maintenances as $assetmaintenance) {
$array[] = self::transformAssetMaintenance($assetmaintenance); $array[] = self::transformMaintenance($assetmaintenance);
} }
return (new DatatablesTransformer)->transformDatatables($array, $total); return (new DatatablesTransformer)->transformDatatables($array, $total);
} }
public function transformAssetMaintenance(AssetMaintenance $assetmaintenance) public function transformMaintenance(Maintenance $assetmaintenance)
{ {
$array = [ $array = [
'id' => (int) $assetmaintenance->id, 'id' => (int) $assetmaintenance->id,
@@ -33,6 +34,7 @@ class AssetMaintenancesTransformer
'created_at' => Helper::getFormattedDateObject($assetmaintenance->asset->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($assetmaintenance->asset->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->asset->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($assetmaintenance->asset->updated_at, 'datetime'),
] : null, ] : null,
'image' => ($assetmaintenance->image != '') ? Storage::disk('public')->url('maintenances/'.e($assetmaintenance->image)) : null,
'model' => (($assetmaintenance->asset) && ($assetmaintenance->asset->model)) ? [ 'model' => (($assetmaintenance->asset) && ($assetmaintenance->asset->model)) ? [
'id' => (int) $assetmaintenance->asset->model->id, 'id' => (int) $assetmaintenance->asset->model->id,
'name'=> ($assetmaintenance->asset->model->name) ? e($assetmaintenance->asset->model->name).' '.e($assetmaintenance->asset->model->model_number) : null, 'name'=> ($assetmaintenance->asset->model->name) ? e($assetmaintenance->asset->model->name).' '.e($assetmaintenance->asset->model->model_number) : null,
@@ -48,7 +50,8 @@ class AssetMaintenancesTransformer
'name'=> ($assetmaintenance->asset->company->name) ? e($assetmaintenance->asset->company->name) : null, 'name'=> ($assetmaintenance->asset->company->name) ? e($assetmaintenance->asset->company->name) : null,
] : null, ] : null,
'title' => ($assetmaintenance->title) ? e($assetmaintenance->title) : null, 'name' => ($assetmaintenance->name) ? e($assetmaintenance->name) : null,
'title' => ($assetmaintenance->name) ? e($assetmaintenance->name) : null, // legacy to not change the shape of the API
'location' => (($assetmaintenance->asset) && ($assetmaintenance->asset->location)) ? [ 'location' => (($assetmaintenance->asset) && ($assetmaintenance->asset->location)) ? [
'id' => (int) $assetmaintenance->asset->location->id, 'id' => (int) $assetmaintenance->asset->location->id,
'name'=> e($assetmaintenance->asset->location->name), 'name'=> e($assetmaintenance->asset->location->name),
@@ -70,11 +73,11 @@ class AssetMaintenancesTransformer
'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'), 'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'),
'user_id' => ($assetmaintenance->adminuser) ? [ 'user_id' => ($assetmaintenance->adminuser) ? [
'id' => $assetmaintenance->adminuser->id, 'id' => $assetmaintenance->adminuser->id,
'name'=> e($assetmaintenance->adminuser->present()->fullName()) 'name'=> e($assetmaintenance->adminuser->display_name)
] : null, // legacy to not change the shape of the API ] : null, // legacy to not change the shape of the API
'created_by' => ($assetmaintenance->adminuser) ? [ 'created_by' => ($assetmaintenance->adminuser) ? [
'id' => (int) $assetmaintenance->adminuser->id, 'id' => (int) $assetmaintenance->adminuser->id,
'name'=> e($assetmaintenance->adminuser->present()->fullName()), 'name'=> e($assetmaintenance->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),

View File

@@ -40,7 +40,7 @@ class ManufacturersTransformer
'notes' => Helper::parseEscapedMarkedownInline($manufacturer->notes), 'notes' => Helper::parseEscapedMarkedownInline($manufacturer->notes),
'created_by' => ($manufacturer->adminuser) ? [ 'created_by' => ($manufacturer->adminuser) ? [
'id' => (int) $manufacturer->adminuser->id, 'id' => (int) $manufacturer->adminuser->id,
'name'=> e($manufacturer->adminuser->present()->fullName()), 'name'=> e($manufacturer->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($manufacturer->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($manufacturer->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($manufacturer->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($manufacturer->updated_at, 'datetime'),

View File

@@ -34,7 +34,7 @@ class PredefinedKitsTransformer
'name' => e($kit->name), 'name' => e($kit->name),
'created_by' => ($kit->adminuser) ? [ 'created_by' => ($kit->adminuser) ? [
'id' => (int) $kit->adminuser->id, 'id' => (int) $kit->adminuser->id,
'name'=> e($kit->adminuser->present()->fullName()), 'name'=> e($kit->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($kit->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($kit->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($kit->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($kit->updated_at, 'datetime'),

View File

@@ -26,7 +26,7 @@ class ProfileTransformer
'id' => (int) $file->id, 'id' => (int) $file->id,
'icon' => Helper::filetype_icon($file->filename), 'icon' => Helper::filetype_icon($file->filename),
'item' => ($file->item) ? [ 'item' => ($file->item) ? [
'name' => ($file->itemType()=='user') ? e($file->item->getFullNameAttribute()) : e($file->item->getDisplayNameAttribute()), 'name' => ($file->itemType()=='user') ? e($file->item->display_name) : e($file->item->getDisplayNameAttribute()),
'type' => e($file->itemType()), 'type' => e($file->itemType()),
] : null, ] : null,
'filename' => e($file->filename), 'filename' => e($file->filename),

View File

@@ -32,7 +32,7 @@ class StatuslabelsTransformer
'notes' => e($statuslabel->notes), 'notes' => e($statuslabel->notes),
'created_by' => ($statuslabel->adminuser) ? [ 'created_by' => ($statuslabel->adminuser) ? [
'id' => (int) $statuslabel->adminuser->id, 'id' => (int) $statuslabel->adminuser->id,
'name'=> e($statuslabel->adminuser->present()->fullName()), 'name'=> e($statuslabel->adminuser->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($statuslabel->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($statuslabel->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($statuslabel->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($statuslabel->updated_at, 'datetime'),

View File

@@ -32,10 +32,11 @@ class UploadedFilesTransformer
'name' => e($file->filename), 'name' => e($file->filename),
'item' => ($file->item_type) ? [ 'item' => ($file->item_type) ? [
'id' => (int) $file->item_id, 'id' => (int) $file->item_id,
'type' => strtolower(class_basename($file->item_type)), 'type' => str_plural(strtolower(class_basename($file->item_type))),
] : null, ] : null,
'filename' => e($file->filename), 'filename' => e($file->filename),
'filetype' => StorageHelper::getFiletype($file->uploads_file_path()), 'filetype' => StorageHelper::getFiletype($file->uploads_file_path()),
'mediatype' => StorageHelper::getMediaType($file->uploads_file_path()),
'url' => $file->uploads_file_url(), 'url' => $file->uploads_file_url(),
'note' => ($file->note) ? e($file->note) : null, 'note' => ($file->note) ? e($file->note) : null,
'created_by' => ($file->adminuser) ? [ 'created_by' => ($file->adminuser) ? [
@@ -44,7 +45,7 @@ class UploadedFilesTransformer
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'), 'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'),
'inline' => StorageHelper::allowSafeInline($file->uploads_file_path()), 'inlineable' => StorageHelper::allowSafeInline($file->uploads_file_path()) ?? false,
'exists_on_disk' => (Storage::exists($file->uploads_file_path()) ? true : false), 'exists_on_disk' => (Storage::exists($file->uploads_file_path()) ? true : false),
]; ];

View File

@@ -22,23 +22,31 @@ class UsersTransformer
public function transformUser(User $user) public function transformUser(User $user)
{ {
$role = '';
if ($user->isSuperUser()) {
$role = 'superadmin';
} elseif ($user->isAdmin()) {
$role = 'admin';
}
$array = [ $array = [
'id' => (int) $user->id, 'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar) ?? null, 'avatar' => e($user->present()->gravatar) ?? null,
'name' => e($user->getFullNameAttribute()), 'name' => e($user->getFullNameAttribute()) ?? null,
'first_name' => e($user->first_name), 'first_name' => e($user->first_name) ?? null,
'last_name' => e($user->last_name), 'last_name' => e($user->last_name) ?? null,
'username' => e($user->username), 'display_name' => e($user->getRawOriginal('display_name')) ?? null,
'username' => e($user->username) ?? null,
'remote' => ($user->remote == '1') ? true : false, 'remote' => ($user->remote == '1') ? true : false,
'locale' => ($user->locale) ? e($user->locale) : null, 'locale' => ($user->locale) ? e($user->locale) : null,
'employee_num' => ($user->employee_num) ? e($user->employee_num) : null, 'employee_num' => ($user->employee_num) ? e($user->employee_num) : null,
'manager' => ($user->manager) ? [ 'manager' => ($user->manager) ? [
'id' => (int) $user->manager->id, 'id' => (int) $user->manager->id,
'name'=> e($user->manager->first_name).' '.e($user->manager->last_name), 'name'=> e($user->manager->display_name),
] : null, ] : null,
'jobtitle' => ($user->jobtitle) ? e($user->jobtitle) : null, 'jobtitle' => ($user->jobtitle) ? e($user->jobtitle) : null,
'vip' => ($user->vip == '1') ? true : false, 'vip' => ($user->vip == '1') ? true : false,
'phone' => ($user->phone) ? e($user->phone) : null, 'phone' => ($user->phone) ? e($user->phone) : null,
'mobile' => ($user->mobile) ? e($user->mobile) : null,
'website' => ($user->website) ? e($user->website) : null, 'website' => ($user->website) ? e($user->website) : null,
'address' => ($user->address) ? e($user->address) : null, 'address' => ($user->address) ? e($user->address) : null,
'city' => ($user->city) ? e($user->city) : null, 'city' => ($user->city) ? e($user->city) : null,
@@ -52,13 +60,14 @@ class UsersTransformer
] : null, ] : null,
'department_manager' => ($user->department?->manager) ? [ 'department_manager' => ($user->department?->manager) ? [
'id' => (int) $user->department->manager->id, 'id' => (int) $user->department->manager->id,
'name'=> e($user->department->manager->full_name), 'name'=> e($user->department->manager->display_name),
] : null, ] : null,
'location' => ($user->userloc) ? [ 'location' => ($user->userloc) ? [
'id' => (int) $user->userloc->id, 'id' => (int) $user->userloc->id,
'name'=> e($user->userloc->name), 'name'=> e($user->userloc->name),
] : null, ] : null,
'notes'=> Helper::parseEscapedMarkedownInline($user->notes), 'notes'=> Helper::parseEscapedMarkedownInline($user->notes),
'role' => $role,
'permissions' => $user->decodePermissions(), 'permissions' => $user->decodePermissions(),
'activated' => ($user->activated == '1') ? true : false, 'activated' => ($user->activated == '1') ? true : false,
'autoassign_licenses' => ($user->autoassign_licenses == '1') ? true : false, 'autoassign_licenses' => ($user->autoassign_licenses == '1') ? true : false,
@@ -74,7 +83,7 @@ class UsersTransformer
'company' => ($user->company) ? ['id' => (int) $user->company->id, 'name'=> e($user->company->name)] : null, 'company' => ($user->company) ? ['id' => (int) $user->company->id, 'name'=> e($user->company->name)] : null,
'created_by' => ($user->createdBy) ? [ 'created_by' => ($user->createdBy) ? [
'id' => (int) $user->createdBy->id, 'id' => (int) $user->createdBy->id,
'name'=> e($user->createdBy->present()->fullName), 'name'=> e($user->createdBy->display_name),
] : null, ] : null,
'created_at' => Helper::getFormattedDateObject($user->created_at, 'datetime'), 'created_at' => Helper::getFormattedDateObject($user->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($user->updated_at, 'datetime'), 'updated_at' => Helper::getFormattedDateObject($user->updated_at, 'datetime'),
@@ -130,6 +139,7 @@ class UsersTransformer
'first_name' => e($user->first_name), 'first_name' => e($user->first_name),
'last_name' => e($user->last_name), 'last_name' => e($user->last_name),
'username' => e($user->username), 'username' => e($user->username),
'display_name' => e($user->display_name),
'created_by' => $user->adminuser ? [ 'created_by' => $user->adminuser ? [
'id' => (int) $user->adminuser->id, 'id' => (int) $user->adminuser->id,
'name'=> e($user->adminuser->present()->fullName), 'name'=> e($user->adminuser->present()->fullName),

View File

@@ -72,6 +72,7 @@ abstract class Importer
'termination_date' => 'termination date', 'termination_date' => 'termination date',
'warranty_months' => 'warranty', 'warranty_months' => 'warranty',
'full_name' => 'full name', 'full_name' => 'full name',
'display_name' => 'display name',
'email' => 'email', 'email' => 'email',
'username' => 'username', 'username' => 'username',
'address' => 'address', 'address' => 'address',
@@ -133,7 +134,7 @@ abstract class Importer
} else { } else {
$this->csv = Reader::createFromString($file); $this->csv = Reader::createFromString($file);
} }
$this->tempPassword = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 40); $this->tempPassword = '*** NO PASSWORD - IMPORTED VIA CSV ***';
} }
// Cached Values for import lookups // Cached Values for import lookups
@@ -299,6 +300,7 @@ abstract class Importer
'full_name' => $this->findCsvMatch($row, 'full_name'), 'full_name' => $this->findCsvMatch($row, 'full_name'),
'first_name' => $this->findCsvMatch($row, 'first_name'), 'first_name' => $this->findCsvMatch($row, 'first_name'),
'last_name' => $this->findCsvMatch($row, 'last_name'), 'last_name' => $this->findCsvMatch($row, 'last_name'),
'display_name' => $this->findCsvMatch($row, 'display_name'),
'email' => $this->findCsvMatch($row, 'email'), 'email' => $this->findCsvMatch($row, 'email'),
'manager_id'=> '', 'manager_id'=> '',
'department_id' => '', 'department_id' => '',
@@ -369,6 +371,7 @@ abstract class Importer
$user->first_name = $user_array['first_name']; $user->first_name = $user_array['first_name'];
$user->last_name = $user_array['last_name']; $user->last_name = $user_array['last_name'];
$user->username = $user_array['username']; $user->username = $user_array['username'];
$user->display_name = $user_array['display_name'] ?? null;
$user->email = $user_array['email']; $user->email = $user_array['email'];
$user->manager_id = $user_array['manager_id'] ?? null; $user->manager_id = $user_array['manager_id'] ?? null;
$user->department_id = $user_array['department_id'] ?? null; $user->department_id = $user_array['department_id'] ?? null;

View File

@@ -7,7 +7,9 @@ use App\Models\Department;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User; use App\Models\User;
use App\Notifications\WelcomeNotification; use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
/** /**
* This is ONLY used for the User Import. When we are importing users * This is ONLY used for the User Import. When we are importing users
@@ -45,11 +47,13 @@ class UserImporter extends ItemImporter
// Pull the records from the CSV to determine their values // Pull the records from the CSV to determine their values
$this->item['id'] = trim($this->findCsvMatch($row, 'id')); $this->item['id'] = trim($this->findCsvMatch($row, 'id'));
$this->item['username'] = trim($this->findCsvMatch($row, 'username')); $this->item['username'] = trim($this->findCsvMatch($row, 'username'));
$this->item['display_name'] = trim($this->findCsvMatch($row, 'display_name'));
$this->item['first_name'] = trim($this->findCsvMatch($row, 'first_name')); $this->item['first_name'] = trim($this->findCsvMatch($row, 'first_name'));
$this->item['last_name'] = trim($this->findCsvMatch($row, 'last_name')); $this->item['last_name'] = trim($this->findCsvMatch($row, 'last_name'));
$this->item['email'] = trim($this->findCsvMatch($row, 'email')); $this->item['email'] = trim($this->findCsvMatch($row, 'email'));
$this->item['gravatar'] = trim($this->findCsvMatch($row, 'gravatar')); $this->item['gravatar'] = trim($this->findCsvMatch($row, 'gravatar'));
$this->item['phone'] = trim($this->findCsvMatch($row, 'phone_number')); $this->item['phone'] = trim($this->findCsvMatch($row, 'phone_number'));
$this->item['mobile'] = trim($this->findCsvMatch($row, 'mobile_number'));
$this->item['website'] = trim($this->findCsvMatch($row, 'website')); $this->item['website'] = trim($this->findCsvMatch($row, 'website'));
$this->item['jobtitle'] = trim($this->findCsvMatch($row, 'jobtitle')); $this->item['jobtitle'] = trim($this->findCsvMatch($row, 'jobtitle'));
$this->item['address'] = trim($this->findCsvMatch($row, 'address')); $this->item['address'] = trim($this->findCsvMatch($row, 'address'));
@@ -80,6 +84,7 @@ class UserImporter extends ItemImporter
$this->item['username'] = $user_formatted_array['username']; $this->item['username'] = $user_formatted_array['username'];
} }
// Check if a numeric ID was passed. If it does, use that above all else. // Check if a numeric ID was passed. If it does, use that above all else.
if ((array_key_exists('id', $this->item) && ($this->item['id'] != "") && (is_numeric($this->item['id'])))) { if ((array_key_exists('id', $this->item) && ($this->item['id'] != "") && (is_numeric($this->item['id'])))) {
$user = User::find($this->item['id']); $user = User::find($this->item['id']);
@@ -89,12 +94,25 @@ class UserImporter extends ItemImporter
if ($user) { if ($user) {
// If the user does not want to update existing values, only add new ones, bail out
if (! $this->updating) { if (! $this->updating) {
Log::debug('A matching User '.$this->item['name'].' already exists. '); Log::debug('A matching User '.$this->item['name'].' already exists. ');
return; return;
} }
$this->log('Updating User'); $this->log('Updating User');
// Todo - check that this works
if (!Gate::allows('canEditAuthFields', $user)) {
unset($user->username);
unset($user->email);
unset($user->password);
unset($user->activated);
}
$user->update($this->sanitizeItemForUpdating($user)); $user->update($this->sanitizeItemForUpdating($user));
// Why do we have to do this twice? Update should
$user->save(); $user->save();
// Update the location of any assets checked out to this user // Update the location of any assets checked out to this user
@@ -110,28 +128,32 @@ class UserImporter extends ItemImporter
// This needs to be applied after the update logic, otherwise we'll overwrite user passwords // This needs to be applied after the update logic, otherwise we'll overwrite user passwords
// Issue #5408 // Issue #5408
$this->item['password'] = bcrypt($this->tempPassword); $this->item['password'] = $this->tempPassword;
$this->log('No matching user, creating one'); $this->log('No matching user, creating one');
$user = new User(); $user = new User();
$user->created_by = auth()->id(); $user->created_by = auth()->id();
$user->fill($this->sanitizeItemForStoring($user)); $user->fill($this->sanitizeItemForStoring($user));
// TODO - check for gate here I guess
if ($user->save()) { if ($user->save()) {
$this->log('User '.$this->item['name'].' was created'); $this->log('User '.$this->item['name'].' was created');
if (($user->email) && ($user->activated == '1')) { if (($user->email) && ($user->activated == '1')) {
$data = [
'email' => $user->email,
'username' => $user->username,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'password' => $this->tempPassword,
];
if ($this->send_welcome) { if ($this->send_welcome) {
$user->notify(new WelcomeNotification($data));
try {
$user->notify(new WelcomeNotification($user));
} catch (\Exception $e) {
Log::warning('Could not send welcome notification for user: ' . $e->getMessage());
}
} }
} }
$user = null; $user = null;
$this->item = null; $this->item = null;
@@ -140,9 +162,9 @@ class UserImporter extends ItemImporter
} }
$this->logError($user, 'User'); $this->logError($user, 'User');
return;
} }
/** /**
* Fetch an existing department, or create new if it doesn't exist * Fetch an existing department, or create new if it doesn't exist
* *

View File

@@ -4,10 +4,12 @@ namespace App\Listeners;
use App\Events\CheckoutableCheckedOut; use App\Events\CheckoutableCheckedOut;
use App\Mail\CheckinAccessoryMail; use App\Mail\CheckinAccessoryMail;
use App\Mail\CheckinComponentMail;
use App\Mail\CheckinLicenseMail; use App\Mail\CheckinLicenseMail;
use App\Mail\CheckoutAccessoryMail; use App\Mail\CheckoutAccessoryMail;
use App\Mail\CheckoutAssetMail; use App\Mail\CheckoutAssetMail;
use App\Mail\CheckinAssetMail; use App\Mail\CheckinAssetMail;
use App\Mail\CheckoutComponentMail;
use App\Mail\CheckoutConsumableMail; use App\Mail\CheckoutConsumableMail;
use App\Mail\CheckoutLicenseMail; use App\Mail\CheckoutLicenseMail;
use App\Models\Accessory; use App\Models\Accessory;
@@ -22,9 +24,11 @@ use App\Models\Setting;
use App\Models\User; use App\Models\User;
use App\Notifications\CheckinAccessoryNotification; use App\Notifications\CheckinAccessoryNotification;
use App\Notifications\CheckinAssetNotification; use App\Notifications\CheckinAssetNotification;
use App\Notifications\CheckinComponentNotification;
use App\Notifications\CheckinLicenseSeatNotification; use App\Notifications\CheckinLicenseSeatNotification;
use App\Notifications\CheckoutAccessoryNotification; use App\Notifications\CheckoutAccessoryNotification;
use App\Notifications\CheckoutAssetNotification; use App\Notifications\CheckoutAssetNotification;
use App\Notifications\CheckoutComponentNotification;
use App\Notifications\CheckoutConsumableNotification; use App\Notifications\CheckoutConsumableNotification;
use App\Notifications\CheckoutLicenseSeatNotification; use App\Notifications\CheckoutLicenseSeatNotification;
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ClientException;
@@ -39,7 +43,7 @@ use Osama\LaravelTeamsNotification\TeamsNotification;
class CheckoutableListener class CheckoutableListener
{ {
private array $skipNotificationsFor = [ private array $skipNotificationsFor = [
Component::class, // Component::class,
]; ];
/** /**
@@ -92,7 +96,8 @@ class CheckoutableListener
if (!empty($to)) { if (!empty($to)) {
try { try {
Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable); Mail::to(array_flatten($to))->send($mailable->locale($notifiable->locale));
Mail::to(array_flatten($cc))->send($mailable->locale(Setting::getSettings()->locale));
Log::info('Checkout Mail sent to checkout target'); Log::info('Checkout Mail sent to checkout target');
} catch (ClientException $e) { } catch (ClientException $e) {
Log::debug("Exception caught during checkout email: " . $e->getMessage()); Log::debug("Exception caught during checkout email: " . $e->getMessage());
@@ -145,7 +150,6 @@ class CheckoutableListener
$shouldSendEmailToUser = $this->checkoutableCategoryShouldSendEmail($event->checkoutable); $shouldSendEmailToUser = $this->checkoutableCategoryShouldSendEmail($event->checkoutable);
$shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress(); $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress();
$shouldSendWebhookNotification = $this->shouldSendWebhookNotification(); $shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) { if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
return; return;
} }
@@ -177,7 +181,8 @@ class CheckoutableListener
try { try {
if (!empty($to)) { if (!empty($to)) {
Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable); Mail::to(array_flatten($to))->send($mailable->locale($notifiable->locale));
Mail::to(array_flatten($cc))->send($mailable->locale(Setting::getSettings()->locale));
Log::info('Checkin Mail sent to CC addresses'); Log::info('Checkin Mail sent to CC addresses');
} }
} catch (ClientException $e) { } catch (ClientException $e) {
@@ -269,6 +274,9 @@ class CheckoutableListener
case LicenseSeat::class: case LicenseSeat::class:
$notificationClass = CheckinLicenseSeatNotification::class; $notificationClass = CheckinLicenseSeatNotification::class;
break; break;
case Component::class:
$notificationClass = CheckinComponentNotification::class;
break;
} }
Log::debug('Notification class: '.$notificationClass); Log::debug('Notification class: '.$notificationClass);
@@ -299,6 +307,9 @@ class CheckoutableListener
case LicenseSeat::class: case LicenseSeat::class:
$notificationClass = CheckoutLicenseSeatNotification::class; $notificationClass = CheckoutLicenseSeatNotification::class;
break; break;
case Component::class:
$notificationClass = CheckoutComponentNotification::class;
break;
} }
@@ -310,6 +321,7 @@ class CheckoutableListener
Asset::class => CheckoutAssetMail::class, Asset::class => CheckoutAssetMail::class,
LicenseSeat::class => CheckoutLicenseMail::class, LicenseSeat::class => CheckoutLicenseMail::class,
Consumable::class => CheckoutConsumableMail::class, Consumable::class => CheckoutConsumableMail::class,
Component::class => CheckoutComponentMail::class,
]; ];
$mailable= $lookup[get_class($event->checkoutable)]; $mailable= $lookup[get_class($event->checkoutable)];
@@ -322,8 +334,8 @@ class CheckoutableListener
Accessory::class => CheckinAccessoryMail::class, Accessory::class => CheckinAccessoryMail::class,
Asset::class => CheckinAssetMail::class, Asset::class => CheckinAssetMail::class,
LicenseSeat::class => CheckinLicenseMail::class, LicenseSeat::class => CheckinLicenseMail::class,
Component::class => CheckinComponentMail::class,
]; ];
$mailable= $lookup[get_class($event->checkoutable)]; $mailable= $lookup[get_class($event->checkoutable)];
return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note); return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
@@ -469,7 +481,8 @@ class CheckoutableListener
return match (true) { return match (true) {
$checkoutable instanceof Asset => $checkoutable->model->category, $checkoutable instanceof Asset => $checkoutable->model->category,
$checkoutable instanceof Accessory, $checkoutable instanceof Accessory,
$checkoutable instanceof Consumable => $checkoutable->category, $checkoutable instanceof Consumable,
$checkoutable instanceof Component => $checkoutable->category,
$checkoutable instanceof LicenseSeat => $checkoutable->license->category, $checkoutable instanceof LicenseSeat => $checkoutable->license->category,
}; };
} }

View File

@@ -334,10 +334,12 @@ class Importer extends Component
'manager_username' => trans('general.importer.manager_username'), 'manager_username' => trans('general.importer.manager_username'),
'notes' => trans('general.notes'), 'notes' => trans('general.notes'),
'phone_number' => trans('admin/users/table.phone'), 'phone_number' => trans('admin/users/table.phone'),
'mobile_number' => trans('admin/users/table.mobile'),
'remote' => trans('admin/users/general.remote'), 'remote' => trans('admin/users/general.remote'),
'start_date' => trans('general.start_date'), 'start_date' => trans('general.start_date'),
'state' => trans('general.state'), 'state' => trans('general.state'),
'username' => trans('admin/users/table.username'), 'username' => trans('admin/users/table.username'),
'display_name' => trans('admin/users/table.display_name'),
'vip' => trans('general.importer.vip'), 'vip' => trans('general.importer.vip'),
'website' => trans('general.website'), 'website' => trans('general.website'),
'zip' => trans('general.zip'), 'zip' => trans('general.zip'),
@@ -484,6 +486,13 @@ class Importer extends Component
'username', 'username',
trans('general.importer.checked_out_to_username'), trans('general.importer.checked_out_to_username'),
], ],
'display_name' =>
[
'display name',
'displayName',
'display',
trans('admin/users/table.display_name'),
],
'first_name' => 'first_name' =>
[ [
'first name', 'first name',
@@ -510,6 +519,13 @@ class Importer extends Component
'telephone', 'telephone',
'tel.', 'tel.',
], ],
'mobile_number' =>
[
'mobile',
'mobile number',
'cell',
'cellphone',
],
'serial' => 'serial' =>
[ [

View File

@@ -47,7 +47,7 @@ class CheckinAssetMail extends Mailable
return new Envelope( return new Envelope(
from: $from, from: $from,
subject: trans('mail.Asset_Checkin_Notification'), subject: trans('mail.Asset_Checkin_Notification', ['tag' => $this->item->asset_tag]),
); );
} }

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Mail;
use App\Models\Accessory;
use App\Models\Component;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CheckinComponentMail extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(Component $component, $checkedOutTo, User $checkedInby, $note)
{
$this->item = $component;
$this->target = $checkedOutTo;
$this->admin = $checkedInby;
$this->note = $note;
$this->settings = Setting::getSettings();
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
$from = new Address(config('mail.from.address'), config('mail.from.name'));
return new Envelope(
from: $from,
subject: trans('mail.Confirm_component_checkin'),
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'mail.markdown.checkin-component',
with: [
'item' => $this->item,
'admin' => $this->admin,
'note' => $this->note,
'target' => $this->target,
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Mail; namespace App\Mail;
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\Asset;
use App\Models\Location;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User; use App\Models\User;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@@ -41,7 +43,7 @@ class CheckoutAccessoryMail extends Mailable
return new Envelope( return new Envelope(
from: $from, from: $from,
subject: (trans('mail.Accessory_Checkout_Notification')), subject: trans('mail.Accessory_Checkout_Notification'),
); );
} }
@@ -54,6 +56,17 @@ class CheckoutAccessoryMail extends Mailable
$eula = $this->item->getEula(); $eula = $this->item->getEula();
$req_accept = $this->item->requireAcceptance(); $req_accept = $this->item->requireAcceptance();
$accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance); $accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
$name = null;
if($this->target instanceof User){
$name = $this->target->display_name;
}
else if($this->target instanceof Asset){
$name = $this->target->assignedto?->display_name;
}
else if($this->target instanceof Location){
$name = $this->target->manager->name;
}
return new Content( return new Content(
markdown: 'mail.markdown.checkout-accessory', markdown: 'mail.markdown.checkout-accessory',
@@ -61,14 +74,35 @@ class CheckoutAccessoryMail extends Mailable
'item' => $this->item, 'item' => $this->item,
'admin' => $this->admin, 'admin' => $this->admin,
'note' => $this->note, 'note' => $this->note,
'target' => $this->target, 'target' => $name,
'eula' => $eula, 'eula' => $eula,
'req_accept' => $req_accept, 'req_accept' => $req_accept,
'accept_url' => $accept_url, 'accept_url' => $accept_url,
'checkout_qty' => $this->checkout_qty, 'checkout_qty' => $this->checkout_qty,
'introduction_line' => $this->introductionLine(),
], ],
); );
} }
private function introductionLine(): string
{
if ($this->target instanceof Location) {
return trans('mail.new_item_checked_location', ['location' => $this->target->name ]);
}
if ($this->requiresAcceptance()) {
return trans('mail.new_item_checked_with_acceptance');
}
if (!$this->requiresAcceptance()) {
return trans('mail.new_item_checked');
}
// we shouldn't get here but let's send a default message just in case
return trans('new_item_checked');
}
private function requiresAcceptance(): int|bool
{
return method_exists($this->item, 'requireAcceptance') ? $this->item->requireAcceptance() : 0;
}
/** /**
* Get the attachments for the message. * Get the attachments for the message.

View File

@@ -4,6 +4,7 @@ namespace App\Mail;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Models\Asset; use App\Models\Asset;
use App\Models\Location;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User; use App\Models\User;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@@ -24,16 +25,17 @@ class CheckoutAssetMail extends Mailable
/** /**
* Create a new message instance. * Create a new message instance.
* @throws \Exception
*/ */
public function __construct(Asset $asset, $checkedOutTo, User $checkedOutBy, $acceptance, $note, bool $firstTimeSending = true) public function __construct(Asset $asset, $checkedOutTo, User $checkedOutBy, $acceptance, $note, bool $firstTimeSending = true)
{ {
$this->item = $asset; $this->item = $asset;
$this->admin = $checkedOutBy; $this->admin = $checkedOutBy;
$this->note = $note; $this->note = $note;
$this->target = $checkedOutTo;
$this->acceptance = $acceptance; $this->acceptance = $acceptance;
$this->settings = Setting::getSettings(); $this->settings = Setting::getSettings();
$this->target = $checkedOutTo;
$this->last_checkout = ''; $this->last_checkout = '';
$this->expected_checkin = ''; $this->expected_checkin = '';
@@ -76,6 +78,17 @@ class CheckoutAssetMail extends Mailable
$eula = method_exists($this->item, 'getEula') ? $this->item->getEula() : ''; $eula = method_exists($this->item, 'getEula') ? $this->item->getEula() : '';
$req_accept = $this->requiresAcceptance(); $req_accept = $this->requiresAcceptance();
$fields = []; $fields = [];
$name = null;
if($this->target instanceof User){
$name = $this->target->display_name;
}
else if($this->target instanceof Asset){
$name = $this->target->assignedto?->display_name;
}
else if($this->target instanceof Location){
$name = $this->target->manager->name;
}
// Check if the item has custom fields associated with it // Check if the item has custom fields associated with it
if (($this->item->model) && ($this->item->model->fieldset)) { if (($this->item->model) && ($this->item->model->fieldset)) {
@@ -91,7 +104,7 @@ class CheckoutAssetMail extends Mailable
'admin' => $this->admin, 'admin' => $this->admin,
'status' => $this->item->assetstatus?->name, 'status' => $this->item->assetstatus?->name,
'note' => $this->note, 'note' => $this->note,
'target' => $this->target, 'target' => $name,
'fields' => $fields, 'fields' => $fields,
'eula' => $eula, 'eula' => $eula,
'req_accept' => $req_accept, 'req_accept' => $req_accept,
@@ -116,7 +129,7 @@ class CheckoutAssetMail extends Mailable
private function getSubject(): string private function getSubject(): string
{ {
if ($this->firstTimeSending) { if ($this->firstTimeSending) {
return trans('mail.Asset_Checkout_Notification'); return trans('mail.Asset_Checkout_Notification', ['tag' => $this->item->asset_tag]);
} }
return trans('mail.unaccepted_asset_reminder'); return trans('mail.unaccepted_asset_reminder');
@@ -124,6 +137,9 @@ class CheckoutAssetMail extends Mailable
private function introductionLine(): string private function introductionLine(): string
{ {
if ($this->firstTimeSending && $this->target instanceof Location) {
return trans('mail.new_item_checked_location', ['location' => $this->target->name ]);
}
if ($this->firstTimeSending && $this->requiresAcceptance()) { if ($this->firstTimeSending && $this->requiresAcceptance()) {
return trans('mail.new_item_checked_with_acceptance'); return trans('mail.new_item_checked_with_acceptance');
} }

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Mail;
use App\Models\Component;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CheckoutComponentMail extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(Component $component, $checkedOutTo, User $checkedOutBy, $acceptance, $note)
{
$this->item = $component;
$this->admin = $checkedOutBy;
$this->note = $note;
$this->target = $checkedOutTo;
$this->acceptance = $acceptance;
$this->qty = $component->assets->first()?->pivot?->assigned_qty;
$this->settings = Setting::getSettings();
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
$from = new Address(config('mail.from.address'), config('mail.from.name'));
return new Envelope(
from: $from,
subject: trans('mail.Confirm_component_delivery'),
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
$eula = $this->item->getEula();
$req_accept = $this->item->requireAcceptance();
$accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
return new Content(
markdown: 'mail.markdown.checkout-component',
with: [
'item' => $this->item,
'admin' => $this->admin,
'note' => $this->note,
'target' => $this->target,
'eula' => $eula,
'req_accept' => $req_accept,
'accept_url' => $accept_url,
'qty' => $this->qty,
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Mail; namespace App\Mail;
use App\Models\Asset;
use App\Models\LicenseSeat; use App\Models\LicenseSeat;
use App\Models\Setting; use App\Models\Setting;
use App\Models\User; use App\Models\User;
@@ -25,9 +26,16 @@ class CheckoutLicenseMail extends Mailable
$this->item = $licenseSeat; $this->item = $licenseSeat;
$this->admin = $checkedOutBy; $this->admin = $checkedOutBy;
$this->note = $note; $this->note = $note;
$this->target = $checkedOutTo;
$this->acceptance = $acceptance; $this->acceptance = $acceptance;
$this->settings = Setting::getSettings(); $this->settings = Setting::getSettings();
$this->target = $checkedOutTo;
if($this->target instanceof User){
$this->target = $this->target->display_name;
}
elseif($this->target instanceof Asset){
$this->target = $this->target->display_name;
}
} }
/** /**

View File

@@ -7,6 +7,7 @@ use App\Presenters\Presentable;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
/** /**
* Model for the Actionlog (the table that keeps a historical log of * Model for the Actionlog (the table that keeps a historical log of
@@ -74,6 +75,8 @@ class Actionlog extends SnipeModel
'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'], 'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'],
'assets.model' => ['name', 'model_number', 'eol', 'notes'], 'assets.model' => ['name', 'model_number', 'eol', 'notes'],
'assets.model.category' => ['name', 'notes'], 'assets.model.category' => ['name', 'notes'],
'assets.location' => ['name'],
'assets.defaultLoc' => ['name'],
'assets.model.manufacturer' => ['name', 'notes'], 'assets.model.manufacturer' => ['name', 'notes'],
'licenses' => ['name', 'serial', 'notes', 'order_number', 'license_email', 'license_name', 'purchase_order', 'purchase_date'], 'licenses' => ['name', 'serial', 'notes', 'order_number', 'license_email', 'license_name', 'purchase_order', 'purchase_date'],
'licenses.category' => ['name', 'notes'], 'licenses.category' => ['name', 'notes'],
@@ -455,38 +458,40 @@ class Actionlog extends SnipeModel
public function uploads_file_url() public function uploads_file_url()
{ {
switch ($this->item_type) {
case Accessory::class:
return route('show.accessoryfile', [$this->item_id, $this->id]); if (($this->action_type == 'accepted') || ($this->action_type == 'declined')) {
case Asset::class: return route('log.storedeula.download', ['filename' => $this->filename]);
return route('show/assetfile', [$this->item_id, $this->id]);
case AssetModel::class:
return route('show/modelfile', [$this->item_id, $this->id]);
case Consumable::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
case Component::class:
return route('show.componentfile', [$this->item_id, $this->id]);
case License::class:
return route('show.licensefile', [$this->item_id, $this->id]);
case Location::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
case User::class:
return route('show/userfile', [$this->item_id, $this->id]);
default:
return null;
} }
$object = Str::snake(str_plural(str_replace("App\Models\\", '', $this->item_type)));
if ($object == 'asset_models') {
$object = 'models';
}
return route('ui.files.show', [
'object_type' => $object,
'id' => $this->item_id,
'file_id' => $this->id,
]);
} }
public function uploads_file_path() public function uploads_file_path()
{ {
if (($this->action_type == 'accepted') || ($this->action_type == 'declined')) {
return 'private_uploads/eula-pdfs/'.$this->filename;
}
switch ($this->item_type) { switch ($this->item_type) {
case Accessory::class: case Accessory::class:
return 'private_uploads/accessories/'.$this->filename; return 'private_uploads/accessories/'.$this->filename;
case Asset::class: case Asset::class:
return 'private_uploads/assets/'.$this->filename; return 'private_uploads/assets/'.$this->filename;
case AssetModel::class: case AssetModel::class:
return 'private_uploads/assetmodels/'.$this->filename; return 'private_uploads/models/'.$this->filename;
case Consumable::class: case Consumable::class:
return 'private_uploads/consumables/'.$this->filename; return 'private_uploads/consumables/'.$this->filename;
case Component::class: case Component::class:
@@ -495,6 +500,8 @@ class Actionlog extends SnipeModel
return 'private_uploads/licenses/'.$this->filename; return 'private_uploads/licenses/'.$this->filename;
case Location::class: case Location::class:
return 'private_uploads/locations/'.$this->filename; return 'private_uploads/locations/'.$this->filename;
case Maintenance::class:
return 'private_uploads/maintenances/'.$this->filename;
case User::class: case User::class:
return 'private_uploads/users/'.$this->filename; return 'private_uploads/users/'.$this->filename;
default: default:
@@ -503,11 +510,6 @@ class Actionlog extends SnipeModel
} }
// Manually sets $this->source for determineActionSource() // Manually sets $this->source for determineActionSource()
public function setActionSource($source = null): void public function setActionSource($source = null): void
{ {

View File

@@ -15,6 +15,8 @@ use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait; use Watson\Validating\ValidatingTrait;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -110,7 +112,7 @@ class Asset extends Depreciable
'location_id' => ['nullable', 'exists:locations,id', 'fmcs_location'], 'location_id' => ['nullable', 'exists:locations,id', 'fmcs_location'],
'rtd_location_id' => ['nullable', 'exists:locations,id', 'fmcs_location'], 'rtd_location_id' => ['nullable', 'exists:locations,id', 'fmcs_location'],
'purchase_date' => ['nullable', 'date', 'date_format:Y-m-d'], 'purchase_date' => ['nullable', 'date', 'date_format:Y-m-d'],
'serial' => ['nullable', 'unique_undeleted:assets,serial'], 'serial' => ['nullable', 'string', 'unique_undeleted:assets,serial'],
'purchase_cost' => ['nullable', 'numeric', 'gte:0', 'max:9999999999999'], 'purchase_cost' => ['nullable', 'numeric', 'gte:0', 'max:9999999999999'],
'supplier_id' => ['nullable', 'exists:suppliers,id'], 'supplier_id' => ['nullable', 'exists:suppliers,id'],
'asset_eol_date' => ['nullable', 'date'], 'asset_eol_date' => ['nullable', 'date'],
@@ -204,6 +206,17 @@ class Asset extends Depreciable
'model.manufacturer' => ['name'], 'model.manufacturer' => ['name'],
]; ];
protected static function booted(): void
{
static::forceDeleted(function (Asset $asset) {
$asset->requests()->forceDelete();
});
static::softDeleted(function (Asset $asset) {
$asset->requests()->delete();
});
}
// To properly set the expected checkin as Y-m-d // To properly set the expected checkin as Y-m-d
public function setExpectedCheckinAttribute($value) public function setExpectedCheckinAttribute($value)
{ {
@@ -224,7 +237,11 @@ class Asset extends Depreciable
foreach ($this->model->fieldset->fields as $field) { foreach ($this->model->fieldset->fields as $field) {
if ($field->format == 'BOOLEAN') { // this just casts booleans that may come through as strings to an actual boolean type
// adding !$field->field_encrypted because when the encrypted value comes through it
// screws things up for the encrypted validation rules (and the encrypted string
// is not a valid boolean type)
if ($field->format == 'BOOLEAN' && !$field->field_encrypted) {
$this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN); $this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN);
} }
} }
@@ -421,12 +438,34 @@ class Asset extends Depreciable
{ {
// Check to see if any of the custom fields were included on the form and if they have any values // Check to see if any of the custom fields were included on the form and if they have any values
if (($this->model) && ($this->model->fieldset) && ($this->model->fieldset->fields)) { if (($this->model) && ($this->model->fieldset) && ($this->model->fieldset->fields)) {
foreach ($this->model->fieldset->fields as $field) { foreach ($this->model->fieldset->fields as $field) {
if (($field->{$checkin_checkout} == 1) && (request()->has($field->db_column))) { if (($field->{$checkin_checkout} == 1) && (request()->has($field->db_column))) {
$this->{$field->db_column} = request()->get($field->db_column);
if ($field->field_encrypted == '1') {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array(request()->input($field->db_column))) {
$this->{$field->db_column} = Crypt::encrypt(implode(', ', request()->input($field->db_column)));
} else {
$this->{$field->db_column} = Crypt::encrypt(request()->get($field->db_column));
}
}
} else {
if (is_array(request()->input($field->db_column))) {
$this->{$field->db_column} = implode(', ', request()->input($field->db_column));
} else {
$this->{$field->db_column} = request()->input($field->db_column);
}
}
} }
} }
} }
} }
@@ -732,9 +771,9 @@ class Asset extends Depreciable
* @since 1.0 * @since 1.0
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function assetmaintenances() public function maintenances()
{ {
return $this->hasMany(\App\Models\AssetMaintenance::class, 'asset_id') return $this->hasMany(\App\Models\Maintenance::class, 'asset_id')
->orderBy('created_at', 'desc'); ->orderBy('created_at', 'desc');
} }

View File

@@ -98,8 +98,16 @@ class AssetModel extends SnipeModel
'manufacturer' => ['name'], 'manufacturer' => ['name'],
]; ];
protected static function booted(): void
{
static::forceDeleted(function (AssetModel $assetModel) {
$assetModel->requests()->forceDelete();
});
static::softDeleted(function (AssetModel $assetModel) {
$assetModel->requests()->delete();
});
}
/** /**
* Establishes the model -> assets relationship * Establishes the model -> assets relationship

View File

@@ -32,7 +32,19 @@ class CheckoutAcceptance extends Model
return array_filter($recipients); return array_filter($recipients);
} }
public function getCheckoutableItemTypeAttribute(): string
{
$type = $this->checkoutable_type;
return match ($type) {
Asset::class => trans('general.asset'),
LicenseSeat::class => trans('general.license'),
Accessory::class => trans('general.accessory'),
Component::class => trans('general.component'),
Consumable::class => trans('general.consumable'),
default => class_basename($type),
};
}
/** /**
* The resource that was is out * The resource that was is out
* *

View File

@@ -2,11 +2,13 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
class CheckoutRequest extends Model class CheckoutRequest extends Model
{ {
use HasFactory;
use SoftDeletes; use SoftDeletes;
protected $fillable = ['user_id']; protected $fillable = ['user_id'];
protected $table = 'checkout_requests'; protected $table = 'checkout_requests';

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