Compare commits

...

428 Commits

Author SHA1 Message Date
snipe
9803e27fc1 Fixed #17642 - location data displaying incrrectly in checkout email intro
Signed-off-by: snipe <snipe@snipe.net>
2025-08-19 08:40:41 +01: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
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
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
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
Godfrey M
64d397c3f3 add component notification tests 2025-07-10 11:26:10 -07:00
snipe
943a4093ad Use standard model transformer for asset model API response
Signed-off-by: snipe <snipe@snipe.net>
2025-07-10 15:42:39 +01: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
1563 changed files with 27996 additions and 312685 deletions

View File

@@ -4189,6 +4189,15 @@
"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"
]
} }
] ]
} }

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

View File

@@ -36,7 +36,7 @@ jobs:
# 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

@@ -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") |
<!-- 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

@@ -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

@@ -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

@@ -1543,11 +1543,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,36 @@ class StorageHelper
} }
} }
public static function getMediaType($file_with_path) {
// The file exists and is allowed to be displayed inline
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';
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 +82,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!='') {
@@ -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

@@ -154,7 +154,7 @@ class AssetModelsController extends Controller
$assetmodel = $request->handleImages($assetmodel); $assetmodel = $request->handleImages($assetmodel);
if ($assetmodel->save()) { if ($assetmodel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.create.success'))); return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.create.success')));
} }
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors())); return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));
@@ -207,7 +207,7 @@ class AssetModelsController extends Controller
$assetmodel = AssetModel::findOrFail($id); $assetmodel = AssetModel::findOrFail($id);
$assetmodel->fill($request->all()); $assetmodel->fill($request->all());
$assetmodel = $request->handleImages($assetmodel); $assetmodel = $request->handleImages($assetmodel);
/** /**
* Allow custom_fieldset_id to override and populate fieldset_id. * Allow custom_fieldset_id to override and populate fieldset_id.
* This is stupid, but required for legacy API support. * This is stupid, but required for legacy API support.
@@ -222,7 +222,7 @@ class AssetModelsController extends Controller
if ($assetmodel->save()) { if ($assetmodel->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $assetmodel, trans('admin/models/message.update.success'))); return response()->json(Helper::formatStandardApiResponse('success', (new AssetModelsTransformer)->transformAssetModel($assetmodel), trans('admin/models/message.update.success')));
} }
return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors())); return response()->json(Helper::formatStandardApiResponse('error', null, $assetmodel->getErrors()));

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.*')

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->present()->fullName()),
] : 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->present()->fullName()),
] : null,
]; ];
} }

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

@@ -150,8 +150,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 +318,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,11 @@ 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 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;
@@ -68,6 +70,7 @@ class UsersController extends Controller
'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 +84,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 +110,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'));
} }
@@ -278,6 +302,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',
@@ -475,8 +500,25 @@ 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('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 +833,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,9 +234,13 @@ 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(['redirect_option' => $request->get('redirect_option'), session()->put(['checkout_to_type' => $request->get('checkout_to_type'),
'checkout_to_type' => $request->get('checkout_to_type'),
'other_redirect' => 'model' ]); 'other_redirect' => 'model' ]);
@@ -412,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')) {
@@ -644,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 = '';
@@ -655,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);
} }

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 = [

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;
@@ -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

@@ -1084,6 +1084,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 +1112,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 +1192,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 +1341,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

@@ -13,15 +13,8 @@ use App\Models\Company;
use App\Models\Group; 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 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\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;
@@ -105,6 +98,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,10 +124,15 @@ 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 ($request->filled('groups')) { if ($request->filled('groups')) {
@@ -142,18 +141,6 @@ class UsersController extends Controller
$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 +235,17 @@ 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->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 +255,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 +263,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 +439,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 +454,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 +471,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);
} }

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

@@ -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

@@ -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,8 +142,10 @@ 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) ? [

View File

@@ -80,7 +80,6 @@ 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) ? [
@@ -204,6 +203,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;
} }

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),

View File

@@ -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),

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()),
'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,6 +22,12 @@ 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,
@@ -39,6 +45,7 @@ class UsersTransformer
'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,
@@ -59,6 +66,7 @@ class UsersTransformer
'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,

View File

@@ -133,7 +133,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

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
@@ -50,6 +52,7 @@ class UserImporter extends ItemImporter
$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 +83,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 +93,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 +127,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 +161,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,
]; ];
/** /**
@@ -145,7 +149,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;
} }
@@ -269,6 +272,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 +305,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 +319,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 +332,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 +479,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,6 +334,7 @@ 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'),
@@ -510,6 +511,13 @@ class Importer extends Component
'telephone', 'telephone',
'tel.', 'tel.',
], ],
'mobile_number' =>
[
'mobile',
'mobile number',
'cell',
'cellphone',
],
'serial' => 'serial' =>
[ [

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

@@ -13,7 +13,6 @@ use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Attachment; use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope; use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class CheckoutAssetMail extends Mailable class CheckoutAssetMail extends Mailable
@@ -24,16 +23,25 @@ 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;
// Location is a target option, but there are no emails currently associated with locations.
if($this->target instanceof User){
$this->target = $this->target->present()?->fullName();
}
else if($this->target instanceof Asset){
$this->target = $this->target->assignedto?->present()?->fullName();
}
$this->last_checkout = ''; $this->last_checkout = '';
$this->expected_checkin = ''; $this->expected_checkin = '';
@@ -91,7 +99,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' => $this->target->name ?? $this->admin->present()->fullName(),
'fields' => $fields, 'fields' => $fields,
'eula' => $eula, 'eula' => $eula,
'req_accept' => $req_accept, 'req_accept' => $req_accept,
@@ -116,7 +124,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').' - '.$this->item->asset_tag;
} }
return trans('mail.unaccepted_asset_reminder'); return trans('mail.unaccepted_asset_reminder');

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->present()?->fullName();
}
elseif($this->target instanceof Asset){
$this->target = $this->target->assignedto?->present()?->fullName();
}
} }
/** /**

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

@@ -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';

View File

@@ -2,12 +2,14 @@
namespace App\Models; namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\HasUploads; use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable; use App\Models\Traits\Searchable;
use App\Presenters\Presentable; use App\Presenters\Presentable;
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\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait; use Watson\Validating\ValidatingTrait;
/** /**
@@ -203,6 +205,36 @@ class Component extends SnipeModel
{ {
return $this->belongsTo(\App\Models\Manufacturer::class, 'manufacturer_id'); return $this->belongsTo(\App\Models\Manufacturer::class, 'manufacturer_id');
} }
/**
* Determine whether this asset requires acceptance by the assigned user
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return bool
*/
public function requireAcceptance()
{
return $this->category->require_acceptance;
}
/**
* Checks for a category-specific EULA, and if that doesn't exist,
* checks for a settings level EULA
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return string | false
*/
public function getEula()
{
if ($this->category->eula_text) {
return Helper::parseEscapedMarkedown($this->category->eula_text);
} elseif ((Setting::getSettings()->default_eula_text) && ($this->category->use_default_eula == '1')) {
return Helper::parseEscapedMarkedown(Setting::getSettings()->default_eula_text);
} else {
return null;
}
}
/** /**
* Establishes the component -> action logs relationship * Establishes the component -> action logs relationship
@@ -248,6 +280,19 @@ class Component extends SnipeModel
} }
/**
* Determine whether to send a checkin/checkout email based on
* asset model category
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @return bool
*/
public function checkin_email()
{
return $this->category?->checkin_email;
}
/** /**
* Check how many items within a component are remaining * Check how many items within a component are remaining

View File

@@ -230,11 +230,16 @@ class Consumable extends SnipeModel
*/ */
public function getImageUrl() public function getImageUrl()
{ {
// If there is a consumable image, use that
if ($this->image) { if ($this->image) {
return Storage::disk('public')->url(app('consumables_upload_path').$this->image); return Storage::disk('public')->url(app('consumables_upload_path').$this->image);
}
return false;
// Otherwise check for a category image
} elseif (($this->category) && ($this->category->image)) {
return Storage::disk('public')->url(app('categories_upload_path').e($this->category->image));
}
return false;
} }
/** /**

View File

@@ -16,6 +16,7 @@ class CustomField extends Model
UniqueUndeletedTrait; UniqueUndeletedTrait;
/** /**
*
* Custom field predfined formats * Custom field predfined formats
* *
* @var array * @var array

View File

@@ -3,12 +3,19 @@
namespace App\Models; namespace App\Models;
use App\Rules\AlphaEncrypted; use App\Rules\AlphaEncrypted;
use App\Rules\BooleanEncrypted;
use App\Rules\DateEncrypted;
use App\Rules\EmailEncrypted;
use App\Rules\IPEncrypted;
use App\Rules\IPv4Encrypted;
use App\Rules\IPv6Encrypted;
use App\Rules\MacEncrypted;
use App\Rules\NumericEncrypted; use App\Rules\NumericEncrypted;
use App\Rules\RegexEncrypted;
use App\Rules\UrlEncrypted;
use Gate; use Gate;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Watson\Validating\ValidatingTrait; use Watson\Validating\ValidatingTrait;
class CustomFieldset extends Model class CustomFieldset extends Model
@@ -99,7 +106,7 @@ class CustomFieldset extends Model
* @since [v3.0] * @since [v3.0]
* @return array * @return array
*/ */
public function validation_rules() public function validation_rules(): array
{ {
$rules = []; $rules = [];
foreach ($this->fields as $field) { foreach ($this->fields as $field) {
@@ -121,18 +128,65 @@ class CustomFieldset extends Model
$rules[$field->db_column_name()] = $rule; $rules[$field->db_column_name()] = $rule;
// this is to switch the rules to rules specially made for encrypted custom fields that decrypt the value before validating
// these are to replace the standard 'numeric' and 'alpha' rules if the custom field is also encrypted. if ($field->field_encrypted) {
// the values need to be decrypted first, because encrypted strings are alphanumeric
if ($field->format === 'NUMERIC' && $field->field_encrypted) {
$numericKey = array_search('numeric', $rules[$field->db_column_name()]); $numericKey = array_search('numeric', $rules[$field->db_column_name()]);
$rules[$field->db_column_name()][$numericKey] = new NumericEncrypted; $alphaKey = array_search('alpha', $rules[$field->db_column_name()]);
$emailKey = array_search('email', $rules[$field->db_column_name()]);
$dateKey = array_search('date', $rules[$field->db_column_name()]);
$urlKey = array_search('url', $rules[$field->db_column_name()]);
$ipKey = array_search('ip', $rules[$field->db_column_name()]);
$ipv4Key = array_search('ipv4', $rules[$field->db_column_name()]);
$ipv6Key = array_search('ipv6', $rules[$field->db_column_name()]);
$macKey = array_search('regex:/^[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}$/', $rules[$field->db_column_name()]);
$booleanKey = array_search('boolean', $rules[$field->db_column_name()]);
// find objects in array that start with "regex:"
// collect because i couldn't figure how to do this
// with array filter and get keys out of it
$regexCollect = collect($rules[$field->db_column_name()]);
$regexKeys = $regexCollect->filter(function ($value, $key) {
return starts_with($value, 'regex:');
})->keys()->values()->toArray();
switch ($field->format) {
case 'NUMERIC':
$rules[$field->db_column_name()][$numericKey] = new NumericEncrypted;
break;
case 'ALPHA':
$rules[$field->db_column_name()][$alphaKey] = new AlphaEncrypted;
break;
case 'EMAIL':
$rules[$field->db_column_name()][$emailKey] = new EmailEncrypted;
break;
case 'DATE':
$rules[$field->db_column_name()][$dateKey] = new DateEncrypted;
break;
case 'URL':
$rules[$field->db_column_name()][$urlKey] = new UrlEncrypted;
break;
case 'IP':
$rules[$field->db_column_name()][$ipKey] = new IPEncrypted;
break;
case 'IPV4':
$rules[$field->db_column_name()][$ipv4Key] = new IPv4Encrypted;
break;
case 'IPV6':
$rules[$field->db_column_name()][$ipv6Key] = new IPv6Encrypted;
break;
case 'MAC':
$rules[$field->db_column_name()][$macKey] = new MacEncrypted;
break;
case 'BOOLEAN':
$rules[$field->db_column_name()][$booleanKey] = new BooleanEncrypted;
break;
case starts_with($field->format, 'regex'):
foreach ($regexKeys as $regexKey) {
$rules[$field->db_column_name()][$regexKey] = new RegexEncrypted;
}
break;
}
} }
if ($field->format === 'ALPHA' && $field->field_encrypted) {
$alphaKey = array_search('alpha', $rules[$field->db_column_name()]);
$rules[$field->db_column_name()][$alphaKey] = new AlphaEncrypted;
}
// add not_array to rules for all fields but checkboxes // add not_array to rules for all fields but checkboxes
if ($field->element != 'checkbox') { if ($field->element != 'checkbox') {

View File

@@ -4,31 +4,34 @@ namespace App\Models;
use App\Helpers\Helper; use App\Helpers\Helper;
use App\Models\Traits\Searchable; use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Watson\Validating\ValidatingTrait; use Watson\Validating\ValidatingTrait;
use App\Models\Traits\HasUploads;
/** /**
* Model for Asset Maintenances. * Model for Asset Maintenances.
* *
* @version v1.0 * @version v1.0
*/ */
class AssetMaintenance extends Model implements ICompanyableChild class Maintenance extends SnipeModel implements ICompanyableChild
{ {
use HasFactory; use HasFactory;
use HasUploads;
use SoftDeletes; use SoftDeletes;
use CompanyableChildTrait; use CompanyableChildTrait;
use ValidatingTrait; use ValidatingTrait;
use Loggable, Presentable;
protected $table = 'asset_maintenances'; protected $table = 'maintenances';
protected $rules = [ protected $rules = [
'asset_id' => 'required|integer', 'asset_id' => 'required|integer',
'supplier_id' => 'nullable|integer', 'supplier_id' => 'nullable|integer',
'asset_maintenance_type' => 'required', 'asset_maintenance_type' => 'required',
'title' => 'required|max:100', 'name' => 'required|max:100',
'is_warranty' => 'boolean', 'is_warranty' => 'boolean',
'start_date' => 'required|date_format:Y-m-d', 'start_date' => 'required|date_format:Y-m-d',
'completion_date' => 'date_format:Y-m-d|nullable|after_or_equal:start_date', 'completion_date' => 'date_format:Y-m-d|nullable|after_or_equal:start_date',
@@ -43,7 +46,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'title', 'name',
'asset_id', 'asset_id',
'supplier_id', 'supplier_id',
'asset_maintenance_type', 'asset_maintenance_type',
@@ -64,7 +67,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
*/ */
protected $searchableAttributes = protected $searchableAttributes =
[ [
'title', 'name',
'notes', 'notes',
'asset_maintenance_type', 'asset_maintenance_type',
'cost', 'cost',
@@ -100,14 +103,14 @@ class AssetMaintenance extends Model implements ICompanyableChild
public static function getImprovementOptions() public static function getImprovementOptions()
{ {
return [ return [
trans('admin/asset_maintenances/general.maintenance') => trans('admin/asset_maintenances/general.maintenance'), trans('admin/maintenances/general.maintenance') => trans('admin/maintenances/general.maintenance'),
trans('admin/asset_maintenances/general.repair') => trans('admin/asset_maintenances/general.repair'), trans('admin/maintenances/general.repair') => trans('admin/maintenances/general.repair'),
trans('admin/asset_maintenances/general.upgrade') => trans('admin/asset_maintenances/general.upgrade'), trans('admin/maintenances/general.upgrade') => trans('admin/maintenances/general.upgrade'),
trans('admin/asset_maintenances/general.pat_test') => trans('admin/asset_maintenances/general.pat_test'), trans('admin/maintenances/general.pat_test') => trans('admin/maintenances/general.pat_test'),
trans('admin/asset_maintenances/general.calibration') => trans('admin/asset_maintenances/general.calibration'), trans('admin/maintenances/general.calibration') => trans('admin/maintenances/general.calibration'),
trans('admin/asset_maintenances/general.software_support') => trans('admin/asset_maintenances/general.software_support'), trans('admin/maintenances/general.software_support') => trans('admin/maintenances/general.software_support'),
trans('admin/asset_maintenances/general.hardware_support') => trans('admin/asset_maintenances/general.hardware_support'), trans('admin/maintenances/general.hardware_support') => trans('admin/maintenances/general.hardware_support'),
trans('admin/asset_maintenances/general.configuration_change') => trans('admin/asset_maintenances/general.configuration_change'), trans('admin/maintenances/general.configuration_change') => trans('admin/maintenances/general.configuration_change'),
]; ];
} }
@@ -166,6 +169,21 @@ class AssetMaintenance extends Model implements ICompanyableChild
return $this->belongsTo(\App\Models\Asset::class, 'asset_id') return $this->belongsTo(\App\Models\Asset::class, 'asset_id')
->withTrashed(); ->withTrashed();
} }
/**
* Get the maintenance logs
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v8.2.2]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assetlog()
{
return $this->hasMany(\App\Models\Actionlog::class, 'item_id')
->where('item_type', '=', self::class)
->orderBy('created_at', 'desc')
->withTrashed();
}
/** /**
@@ -187,6 +205,11 @@ class AssetMaintenance extends Model implements ICompanyableChild
->withTrashed(); ->withTrashed();
} }
public function getDisplayNameAttribute()
{
return $this->name;
}
/** /**
* ----------------------------------------------- * -----------------------------------------------
* BEGIN QUERY SCOPES * BEGIN QUERY SCOPES
@@ -203,7 +226,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
*/ */
public function scopeOrderBySupplier($query, $order) public function scopeOrderBySupplier($query, $order)
{ {
return $query->leftJoin('suppliers as suppliers_maintenances', 'asset_maintenances.supplier_id', '=', 'suppliers_maintenances.id') return $query->leftJoin('suppliers as suppliers_maintenances', 'maintenances.supplier_id', '=', 'suppliers_maintenances.id')
->orderBy('suppliers_maintenances.name', $order); ->orderBy('suppliers_maintenances.name', $order);
} }
@@ -219,7 +242,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
*/ */
public function scopeOrderByTag($query, $order) public function scopeOrderByTag($query, $order)
{ {
return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id') return $query->leftJoin('assets', 'maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.asset_tag', $order); ->orderBy('assets.asset_tag', $order);
} }
@@ -233,7 +256,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
*/ */
public function scopeOrderByAssetName($query, $order) public function scopeOrderByAssetName($query, $order)
{ {
return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id') return $query->leftJoin('assets', 'maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.name', $order); ->orderBy('assets.name', $order);
} }
@@ -247,7 +270,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
*/ */
public function scopeOrderByAssetSerial($query, $order) public function scopeOrderByAssetSerial($query, $order)
{ {
return $query->leftJoin('assets', 'asset_maintenances.asset_id', '=', 'assets.id') return $query->leftJoin('assets', 'maintenances.asset_id', '=', 'assets.id')
->orderBy('assets.serial', $order); ->orderBy('assets.serial', $order);
} }
@@ -261,7 +284,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
*/ */
public function scopeOrderStatusName($query, $order) public function scopeOrderStatusName($query, $order)
{ {
return $query->join('assets as maintained_asset', 'asset_maintenances.asset_id', '=', 'maintained_asset.id') return $query->join('assets as maintained_asset', 'maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('status_labels as maintained_asset_status', 'maintained_asset_status.id', '=', 'maintained_asset.status_id') ->leftjoin('status_labels as maintained_asset_status', 'maintained_asset_status.id', '=', 'maintained_asset.status_id')
->orderBy('maintained_asset_status.name', $order); ->orderBy('maintained_asset_status.name', $order);
} }
@@ -276,7 +299,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
*/ */
public function scopeOrderLocationName($query, $order) public function scopeOrderLocationName($query, $order)
{ {
return $query->join('assets as maintained_asset', 'asset_maintenances.asset_id', '=', 'maintained_asset.id') return $query->join('assets as maintained_asset', 'maintenances.asset_id', '=', 'maintained_asset.id')
->leftjoin('locations as maintained_asset_location', 'maintained_asset_location.id', '=', 'maintained_asset.location_id') ->leftjoin('locations as maintained_asset_location', 'maintained_asset_location.id', '=', 'maintained_asset.location_id')
->orderBy('maintained_asset_location.name', $order); ->orderBy('maintained_asset_location.name', $order);
} }
@@ -286,6 +309,6 @@ class AssetMaintenance extends Model implements ICompanyableChild
*/ */
public function scopeOrderByCreatedBy($query, $order) public function scopeOrderByCreatedBy($query, $order)
{ {
return $query->leftJoin('users as admin_sort', 'asset_maintenances.created_by', '=', 'admin_sort.id')->select('asset_maintenances.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order); return $query->leftJoin('users as admin_sort', 'maintenances.created_by', '=', 'admin_sort.id')->select('maintenances.*')->orderBy('admin_sort.first_name', $order)->orderBy('admin_sort.last_name', $order);
} }
} }

View File

@@ -7,7 +7,7 @@ use App\Models\Traits\Searchable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Watson\Validating\ValidatingTrait; use Watson\Validating\ValidatingTrait;
use \Illuminate\Database\Eloquent\Relations\Relation;
class Supplier extends SnipeModel class Supplier extends SnipeModel
{ {
use HasFactory; use HasFactory;
@@ -133,7 +133,7 @@ class Supplier extends SnipeModel
* Establishes the supplier -> admin user relationship * Establishes the supplier -> admin user relationship
* *
* @author A. Gianotto <snipe@snipe.net> * @author A. Gianotto <snipe@snipe.net>
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return Relation
*/ */
public function adminuser() public function adminuser()
{ {
@@ -147,9 +147,9 @@ class Supplier extends SnipeModel
* @since [v1.0] * @since [v1.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation * @return \Illuminate\Database\Eloquent\Relations\Relation
*/ */
public function asset_maintenances() public function maintenances(): Relation
{ {
return $this->hasMany(\App\Models\AssetMaintenance::class, 'supplier_id'); return $this->hasMany(\App\Models\Maintenance::class, 'supplier_id');
} }
/** /**

View File

@@ -70,6 +70,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
'manager_id', 'manager_id',
'password', 'password',
'phone', 'phone',
'mobile',
'notes', 'notes',
'state', 'state',
'username', 'username',
@@ -139,6 +140,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
'locale', 'locale',
'notes', 'notes',
'phone', 'phone',
'mobile',
'state', 'state',
'username', 'username',
'website', 'website',
@@ -183,6 +185,17 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
); );
} }
protected static function booted(): void
{
static::forceDeleted(function (User $user) {
CheckoutRequest::where(['user_id' => $user->id])->forceDelete();
});
static::softDeleted(function (User $user) {
CheckoutRequest::where(['user_id' => $user->id])->delete();
});
}
public function isAvatarExternal() public function isAvatarExternal()
{ {
@@ -203,11 +216,19 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
{ {
$user_groups = $this->groups; $user_groups = $this->groups;
if (($this->permissions == '') && (count($user_groups) == 0)) { if (($this->permissions == '') && (count($user_groups) == 0)) {
return false; return false;
} }
$user_permissions = json_decode($this->permissions, true); $user_permissions = $this->permissions;
if (is_object($this->permissions)) {
$user_permissions = json_decode(json_encode($this->permissions), true);
}
if (is_string($this->permissions)) {
$user_permissions = json_decode($this->permissions, true);
}
$is_user_section_permissions_set = ($user_permissions != '') && array_key_exists($section, $user_permissions); $is_user_section_permissions_set = ($user_permissions != '') && array_key_exists($section, $user_permissions);
//If the user is explicitly granted, return true //If the user is explicitly granted, return true
@@ -261,6 +282,18 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $this->checkPermissionSection('superuser'); return $this->checkPermissionSection('superuser');
} }
/**
* Checks if the user is an admin
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v8.1.18]
* @return bool
*/
public function isAdmin()
{
return $this->checkPermissionSection('admin');
}
/** /**
* Checks if the user can edit their own profile * Checks if the user can edit their own profile
@@ -288,13 +321,15 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
*/ */
public function isDeletable() public function isDeletable()
{ {
return Gate::allows('delete', $this) return Gate::allows('delete', $this)
&& ($this->assets->count() === 0) && (($this->assets_count ?? $this->assets()->count()) === 0)
&& ($this->licenses->count() === 0) && (($this->accessories_count ?? $this->accessories()->count()) === 0)
&& ($this->consumables->count() === 0) && (($this->licenses_count ?? $this->licenses()->count()) === 0)
&& ($this->accessories->count() === 0) && (($this->consumables_count ?? $this->consumables()->count()) === 0)
&& ($this->managedLocations->count() === 0) && (($this->accessories_count ?? $this->accessories()->count()) === 0)
&& ($this->managesUsers->count() === 0) && (($this->manages_users_count ?? $this->managesUsers()->count()) === 0)
&& (($this->manages_locations_count ?? $this->managedLocations()->count()) === 0)
&& ($this->deleted_at == ''); && ($this->deleted_at == '');
} }
@@ -376,9 +411,9 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
* @since [v4.0] * @since [v4.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, 'user_id')->withTrashed(); return $this->hasMany(\App\Models\Maintenance::class, 'user_id')->withTrashed();
} }
/** /**
@@ -869,6 +904,49 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
); );
} }
/**
* Return only admins and superusers
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
*/
public function scopeOnlySuperAdmins($query)
{
return $query->where('users.permissions', 'LIKE', '%"superuser":"1"%')
->orWhere('users.permissions', 'LIKE', '%"superuser":1%')
->orWhereHas(
'groups', function ($query) {
$query->where('permission_groups.permissions', 'LIKE', '%"superuser":"1"%')
->orWhere('permission_groups.permissions', 'LIKE', '%"superuser":1%');
}
);
}
/**
* Return only admins and superusers
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
*/
public function scopeOnlyAdminsAndSuperAdmins($query)
{
return $query->where('users.permissions', 'LIKE', '%"superuser":"1"%')
->orWhere('users.permissions', 'LIKE', '%"superuser":1%')
->orWhere('users.permissions', 'LIKE', '%"admin":1%')
->orWhere('users.permissions', 'LIKE', '%"admin":"1"%')
->orWhereHas(
'groups', function ($query) {
$query->where('permission_groups.permissions', 'LIKE', '%"superuser":"1"%')
->orWhere('permission_groups.permissions', 'LIKE', '%"superuser":1%')
->orWhere('permission_groups.permissions', 'LIKE', '%"admin":1%')
->orWhere('permission_groups.permissions', 'LIKE', '%"admin":"1"%');
}
);
}
/** /**
* Query builder scope to order on manager * Query builder scope to order on manager
@@ -942,7 +1020,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
/** /**
* Get the preferred locale for the user. * Get the preferred locale for the user.
* *
@@ -981,7 +1058,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
public function scopeUserLocation($query, $location, $search) public function scopeUserLocation($query, $location, $search)
{ {
return $query->where('location_id', '=', $location) return $query->where('location_id', '=', $location)
->where('users.first_name', 'LIKE', '%' . $search . '%') ->where('users.first_name', 'LIKE', '%' . $search . '%')
->orWhere('users.email', 'LIKE', '%' . $search . '%') ->orWhere('users.email', 'LIKE', '%' . $search . '%')
@@ -994,9 +1070,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
->orWhere('users.username', 'LIKE', '%' . $search . '%') ->orWhere('users.username', 'LIKE', '%' . $search . '%')
->orwhereRaw('CONCAT(users.first_name," ",users.last_name) LIKE \''.$search.'%\''); ->orwhereRaw('CONCAT(users.first_name," ",users.last_name) LIKE \''.$search.'%\'');
} }
/** /**

View File

@@ -29,6 +29,7 @@ class AcceptanceAssetAcceptedNotification extends Notification
$this->assigned_to = $params['assigned_to']; $this->assigned_to = $params['assigned_to'];
$this->note = $params['note']; $this->note = $params['note'];
$this->company_name = $params['company_name']; $this->company_name = $params['company_name'];
$this->admin = $params['admin'] ?? null;
$this->settings = Setting::getSettings(); $this->settings = Setting::getSettings();
} }
@@ -72,6 +73,7 @@ class AcceptanceAssetAcceptedNotification extends Notification
'assigned_to' => $this->assigned_to, 'assigned_to' => $this->assigned_to,
'company_name' => $this->company_name, 'company_name' => $this->company_name,
'intro_text' => trans('mail.acceptance_asset_accepted'), 'intro_text' => trans('mail.acceptance_asset_accepted'),
'admin' => $this->admin,
]) ])
->subject(trans('mail.acceptance_asset_accepted')); ->subject(trans('mail.acceptance_asset_accepted'));

View File

@@ -0,0 +1,165 @@
<?php
namespace App\Notifications;
use App\Models\Component;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Channels\SlackWebhookChannel;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
use NotificationChannels\GoogleChat\Card;
use NotificationChannels\GoogleChat\GoogleChatChannel;
use NotificationChannels\GoogleChat\GoogleChatMessage;
use NotificationChannels\GoogleChat\Section;
use NotificationChannels\GoogleChat\Widgets\KeyValue;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage;
class CheckinComponentNotification extends Notification
{
use Queueable;
/**
* @var
*/
private $params;
/**
* Create a new notification instance.
*
* @param $params
*/
public function __construct(Component $component, $checkedOutTo, User $checkedInBy, $note)
{
$this->target = $checkedOutTo;
$this->item = $component;
$this->admin = $checkedInBy;
$this->note = $note;
$this->settings = Setting::getSettings();
}
/**
* Get the notification's delivery channels.
*
* @return array
*/
public function via()
{
$notifyBy = [];
if (Setting::getSettings()->webhook_selected == 'google' && Setting::getSettings()->webhook_endpoint) {
$notifyBy[] = GoogleChatChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'microsoft' && Setting::getSettings()->webhook_endpoint) {
$notifyBy[] = MicrosoftTeamsChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'slack' || Setting::getSettings()->webhook_selected == 'general' ) {
$notifyBy[] = SlackWebhookChannel::class;
}
return $notifyBy;
}
public function toSlack()
{
$target = $this->target;
$admin = $this->admin;
$item = $this->item;
$note = $this->note;
$botname = ($this->settings->webhook_botname) ? $this->settings->webhook_botname : 'Snipe-Bot';
$channel = ($this->settings->webhook_channel) ? $this->settings->webhook_channel : '';
if ($admin) {
$fields = [
trans('general.from') => '<'.$target->present()->viewUrl().'|'.$target->present()->fullName().'>',
trans('general.by') => '<'.$admin->present()->viewUrl().'|'.$admin->present()->fullName().'>',
];
if ($item->location) {
$fields[trans('general.location')] = $item->location->name;
}
if ($item->company) {
$fields[trans('general.company')] = $item->company->name;
}
} else {
$fields = [
'To' => '<'.$target->present()->viewUrl().'|'.$target->present()->fullName().'>',
'By' => 'CLI tool',
];
}
return (new SlackMessage)
->content(':arrow_down: :package: '.trans('mail.Component_checkin_notification'))
->from($botname)
->to($channel)
->attachment(function ($attachment) use ($item, $note, $admin, $fields) {
$attachment->title(htmlspecialchars_decode($item->present()->name), $item->present()->viewUrl())
->fields($fields)
->content($note);
});
}
public function toMicrosoftTeams()
{
$target = $this->target;
$admin = $this->admin;
$item = $this->item;
$note = $this->note;
if(!Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows')) {
return MicrosoftTeamsMessage::create()
->to($this->settings->webhook_endpoint)
->type('success')
->addStartGroupToSection('activityTitle')
->title(trans('mail.Component_checkin_notification'))
->addStartGroupToSection('activityText')
->fact(htmlspecialchars_decode($item->present()->name), '', 'header')
->fact(trans('mail.Component_checkin_notification')." by ", $admin->present()->fullName() ?: 'CLI tool')
->fact(trans('mail.checkedin_from'), $target->present()->fullName())
->fact(trans('admin/consumables/general.remaining'), $item->numRemaining())
->fact(trans('mail.notes'), $note ?: '');
}
$message = trans('mail.Component_checkin_notification');
$details = [
trans('mail.checkedin_from')=> $target->present()->fullName(),
trans('mail.Component_checkin_notification')." by " => $admin->present()->fullName() ?: 'CLI tool',
trans('admin/consumables/general.remaining') => $item->numRemaining(),
trans('mail.notes') => $note ?: '',
];
return array($message, $details);
}
public function toGoogleChat()
{
$target = $this->target;
$item = $this->item;
$note = $this->note;
return GoogleChatMessage::create()
->to($this->settings->webhook_endpoint)
->card(
Card::create()
->header(
'<strong>'.trans('mail.Component_checkin_notification').'</strong>' ?: '',
htmlspecialchars_decode($item->present()->name) ?: '',
)
->section(
Section::create(
KeyValue::create(
trans('mail.checkedin_from') ?: '',
$target->present()->fullName() ?: '',
trans('admin/consumables/general.remaining').': '.$item->numRemaining(),
)
->onClick(route('components.show', $item->id))
)
)
);
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace App\Notifications;
use App\Models\Component;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Channels\SlackWebhookChannel;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Str;
use NotificationChannels\GoogleChat\Card;
use NotificationChannels\GoogleChat\GoogleChatChannel;
use NotificationChannels\GoogleChat\GoogleChatMessage;
use NotificationChannels\GoogleChat\Section;
use NotificationChannels\GoogleChat\Widgets\KeyValue;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel;
use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage;
class CheckoutComponentNotification extends Notification
{
use Queueable;
/**
* @var
*/
private $params;
/**
* Create a new notification instance.
*
* @param $params
*/
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->checkout_qty;
$this->settings = Setting::getSettings();
}
/**`
* Get the notification's delivery channels.
*
* @return array
*/
public function via()
{
$notifyBy = [];
if (Setting::getSettings()->webhook_selected == 'google' && Setting::getSettings()->webhook_endpoint) {
$notifyBy[] = GoogleChatChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'microsoft' && Setting::getSettings()->webhook_endpoint) {
$notifyBy[] = MicrosoftTeamsChannel::class;
}
if (Setting::getSettings()->webhook_selected == 'slack' || Setting::getSettings()->webhook_selected == 'general' ) {
$notifyBy[] = SlackWebhookChannel::class;
}
return $notifyBy;
}
public function toSlack()
{
$target = $this->target;
$admin = $this->admin;
$item = $this->item;
$note = $this->note;
$botname = ($this->settings->webhook_botname) ? $this->settings->webhook_botname : 'Snipe-Bot';
$channel = ($this->settings->webhook_channel) ? $this->settings->webhook_channel : '';
$fields = [
trans('general.to') => '<'.$target->present()->viewUrl().'|'.$target->present()->fullName().'>',
trans('general.by') => '<'.$admin->present()->viewUrl().'|'.$admin->present()->fullName().'>',
];
if ($item->location) {
$fields[trans('general.location')] = $item->location->name;
}
if ($item->company) {
$fields[trans('general.company')] = $item->company->name;
}
return (new SlackMessage)
->content(':arrow_up: :package: '.trans('mail.Component_checkout_notification'))
->from($botname)
->to($channel)
->attachment(function ($attachment) use ($item, $note, $admin, $fields) {
$attachment->title(htmlspecialchars_decode($item->present()->name), $item->present()->viewUrl())
->fields($fields)
->content($note);
});
}
public function toMicrosoftTeams()
{
$target = $this->target;
$admin = $this->admin;
$item = $this->item;
$note = $this->note;
if(!Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows')) {
return MicrosoftTeamsMessage::create()
->to($this->settings->webhook_endpoint)
->type('success')
->addStartGroupToSection('activityTitle')
->title(trans('mail.Component_checkout_notification'))
->addStartGroupToSection('activityText')
->fact(htmlspecialchars_decode($item->present()->name), '', 'activityTitle')
->fact(trans('mail.Component_checkout_notification')." by ", $admin->present()->fullName())
->fact(trans('mail.assigned_to'), $target->present()->fullName())
->fact(trans('admin/consumables/general.remaining'), $item->numRemaining())
->fact(trans('mail.notes'), $note ?: '');
}
$message = trans('mail.Component_checkout_notification');
$details = [
trans('mail.assigned_to') => $target->present()->fullName(),
trans('mail.item') => htmlspecialchars_decode($item->present()->name),
trans('mail.Component_checkout_notification').' by' => $admin->present()->fullName(),
trans('admin/consumables/general.remaining') => $item->numRemaining(),
trans('mail.notes') => $note ?: '',
];
return array($message, $details);
}
public function toGoogleChat()
{
$target = $this->target;
$item = $this->item;
$note = $this->note;
return GoogleChatMessage::create()
->to($this->settings->webhook_endpoint)
->card(
Card::create()
->header(
'<strong>'.trans('mail.Component_checkout_notification').'</strong>' ?: '',
htmlspecialchars_decode($item->present()->name) ?: '',
)
->section(
Section::create(
KeyValue::create(
trans('mail.assigned_to') ?: '',
$target->present()->fullName() ?: '',
trans('admin/consumables/general.remaining').': '.$item->numRemaining(),
)
->onClick(route('api.assets.show', $target->id))
)
)
);
}
}

View File

@@ -5,26 +5,23 @@ namespace App\Notifications;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Password;
use App\Models\User;
class WelcomeNotification extends Notification class WelcomeNotification extends Notification
{ {
use Queueable; use Queueable;
private $_data = []; public $expire_date;
/** /**
* Create a new notification instance. * Create a new notification instance.
* *
* @return void * @return void
*/ */
public function __construct(array $content) public function __construct(public User $user)
{ {
$this->_data['email'] = htmlspecialchars_decode($content['email']); $this->user->token = Password::broker('invites')->createToken($user);
$this->_data['first_name'] = htmlspecialchars_decode($content['first_name']); $this->user->expire_date = now()->addMinutes((int) config('auth.passwords.invites.expire', 2880))->format('F j, Y, g:i a');
$this->_data['last_name'] = htmlspecialchars_decode($content['last_name']);
$this->_data['username'] = htmlspecialchars_decode($content['username']);
$this->_data['password'] = htmlspecialchars_decode($content['password']);
$this->_data['url'] = config('app.url');
} }
/** /**
@@ -44,8 +41,9 @@ class WelcomeNotification extends Notification
*/ */
public function toMail() public function toMail()
{ {
return (new MailMessage()) return (new MailMessage())
->subject(trans('mail.welcome', ['name' => $this->_data['first_name'].' '.$this->_data['last_name']])) ->subject(trans('mail.welcome', ['name' => $this->user->first_name.' '.$this->user->last_name]))
->markdown('notifications.Welcome', $this->_data); ->markdown('notifications.Welcome', $this->user->toArray());
} }
} }

View File

@@ -20,7 +20,11 @@ class ComponentObserver
$logAction->item_type = Component::class; $logAction->item_type = Component::class;
$logAction->item_id = $component->id; $logAction->item_id = $component->id;
$logAction->created_at = date('Y-m-d H:i:s'); $logAction->created_at = date('Y-m-d H:i:s');
$logAction->action_date = date('Y-m-d H:i:s');
$logAction->created_by = auth()->id(); $logAction->created_by = auth()->id();
if($component->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('update'); $logAction->logaction('update');
} }
@@ -37,6 +41,7 @@ class ComponentObserver
$logAction->item_type = Component::class; $logAction->item_type = Component::class;
$logAction->item_id = $component->id; $logAction->item_id = $component->id;
$logAction->created_at = date('Y-m-d H:i:s'); $logAction->created_at = date('Y-m-d H:i:s');
$logAction->action_date = date('Y-m-d H:i:s');
$logAction->created_by = auth()->id(); $logAction->created_by = auth()->id();
if($component->imported) { if($component->imported) {
$logAction->setActionSource('importer'); $logAction->setActionSource('importer');
@@ -56,6 +61,7 @@ class ComponentObserver
$logAction->item_type = Component::class; $logAction->item_type = Component::class;
$logAction->item_id = $component->id; $logAction->item_id = $component->id;
$logAction->created_at = date('Y-m-d H:i:s'); $logAction->created_at = date('Y-m-d H:i:s');
$logAction->action_date = date('Y-m-d H:i:s');
$logAction->created_by = auth()->id(); $logAction->created_by = auth()->id();
$logAction->logaction('delete'); $logAction->logaction('delete');
} }

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Observers;
use App\Models\Actionlog;
use App\Models\Maintenance;
use App\Models\Asset;
class MaintenanceObserver
{
/**
* Listen to the User created event.
*
* @param Maintenance $maintenance
* @return void
*/
public function updated(Maintenance $maintenance)
{
$logAction = new Actionlog();
$logAction->item_type = Maintenance::class;
$logAction->item_id = $maintenance->id;
$logAction->target_type = Asset::class;
$logAction->target_id = $maintenance->asset_id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->action_date = date('Y-m-d H:i:s');
$logAction->created_by = auth()->id();
if($maintenance->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('update');
}
/**
* Listen to the Component created event when
* a new component is created.
*
* @param Maintenance $maintenance
* @return void
*/
public function created(Maintenance $maintenance)
{
$logAction = new Actionlog();
$logAction->item_type = Maintenance::class;
$logAction->item_id = $maintenance->id;
$logAction->target_type = Asset::class;
$logAction->target_id = $maintenance->asset_id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->action_date = date('Y-m-d H:i:s');
$logAction->created_by = auth()->id();
if($maintenance->imported) {
$logAction->setActionSource('importer');
}
$logAction->logaction('create');
}
/**
* Listen to the Component deleting event.
*
* @param Maintenance $maintenance
* @return void
*/
public function deleting(Maintenance $maintenance)
{
$logAction = new Actionlog();
$logAction->item_type = Maintenance::class;
$logAction->item_id = $maintenance->id;
$logAction->target_type = Asset::class;
$logAction->target_id = $maintenance->asset_id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->action_date = date('Y-m-d H:i:s');
$logAction->created_by = auth()->id();
$logAction->logaction('delete');
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Policies; namespace App\Policies;
use App\Models\User; use App\Models\User;
use App\Models\Asset;
class AssetPolicy extends CheckoutablePermissionsPolicy class AssetPolicy extends CheckoutablePermissionsPolicy
{ {

View File

@@ -85,7 +85,7 @@ abstract class SnipePermissionsPolicy
} }
/** /**
* Determine whether the user can view the accessory. * Determine whether the user can view the model.
* *
* @param \App\Models\User $user * @param \App\Models\User $user
* @return mixed * @return mixed
@@ -101,7 +101,7 @@ abstract class SnipePermissionsPolicy
} }
/** /**
* Determine whether the user can create accessories. * Determine whether the user can create model.
* *
* @param \App\Models\User $user * @param \App\Models\User $user
* @return mixed * @return mixed
@@ -112,7 +112,7 @@ abstract class SnipePermissionsPolicy
} }
/** /**
* Determine whether the user can update the accessory. * Determine whether the user can update the model.
* *
* @param \App\Models\User $user * @param \App\Models\User $user
* @return mixed * @return mixed
@@ -124,7 +124,7 @@ abstract class SnipePermissionsPolicy
/** /**
* Determine whether the user can update the accessory. * Determine whether the user can update the model.
* *
* @param \App\Models\User $user * @param \App\Models\User $user
* @return mixed * @return mixed
@@ -135,7 +135,7 @@ abstract class SnipePermissionsPolicy
} }
/** /**
* Determine whether the user can delete the accessory. * Determine whether the user can delete the model.
* *
* @param \App\Models\User $user * @param \App\Models\User $user
* @return mixed * @return mixed
@@ -151,7 +151,7 @@ abstract class SnipePermissionsPolicy
} }
/** /**
* Determine whether the user can manage the accessory. * Determine whether the user can manage the model.
* *
* @param \App\Models\User $user * @param \App\Models\User $user
* @return mixed * @return mixed

View File

@@ -115,6 +115,7 @@ class AssetPresenter extends Presenter
'sortable' => true, 'sortable' => true,
'title' => trans('admin/users/table.title'), 'title' => trans('admin/users/table.title'),
'visible' => false, 'visible' => false,
'formatter' => 'jobtitleFormatter',
], [ ], [
'field' => 'location', 'field' => 'location',
'searchable' => true, 'searchable' => true,
@@ -327,7 +328,7 @@ class AssetPresenter extends Presenter
// name can break the listings page. - snipe // name can break the listings page. - snipe
foreach ($fields as $field) { foreach ($fields as $field) {
$layout[] = [ $layout[] = [
'field' => 'custom_fields.'.$field->db_column, 'field' => $field->db_column,
'searchable' => true, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'switchable' => true, 'switchable' => true,

View File

@@ -106,7 +106,7 @@ class HistoryPresenter extends Presenter
'switchable' => true, 'switchable' => true,
'title' => trans('general.file_name'), 'title' => trans('general.file_name'),
'visible' => true, 'visible' => true,
'formatter' => 'fileUploadNameFormatter', 'formatter' => 'fileNameFormatter',
], ],
[ [
'field' => 'file_download', 'field' => 'file_download',
@@ -115,7 +115,7 @@ class HistoryPresenter extends Presenter
'switchable' => true, 'switchable' => true,
'title' => trans('general.download'), 'title' => trans('general.download'),
'visible' => true, 'visible' => true,
'formatter' => 'fileUploadFormatter', 'formatter' => 'fileDownloadButtonsFormatter',
], ],
[ [
'field' => 'note', 'field' => 'note',

View File

@@ -67,7 +67,9 @@ class LocationPresenter extends Presenter
'sortable' => true, 'sortable' => true,
'switchable' => true, 'switchable' => true,
'title' => trans('admin/locations/message.current_location'), 'title' => trans('admin/locations/message.current_location'),
'titleTooltip' => trans('admin/locations/message.current_location'),
'visible' => true, 'visible' => true,
'class' => 'css-house-laptop',
], [ ], [
'field' => 'rtd_assets_count', 'field' => 'rtd_assets_count',
'searchable' => false, 'searchable' => false,

View File

@@ -5,7 +5,7 @@ namespace App\Presenters;
/** /**
* Class AssetModelPresenter * Class AssetModelPresenter
*/ */
class AssetMaintenancesPresenter extends Presenter class MaintenancesPresenter extends Presenter
{ {
/** /**
* Json Column Layout for bootstrap table * Json Column Layout for bootstrap table
@@ -22,7 +22,7 @@ class AssetMaintenancesPresenter extends Presenter
'title' => trans('general.id'), 'title' => trans('general.id'),
'visible' => false, 'visible' => false,
], [ ], [
'field' => 'title', 'field' => 'name',
'searchable' => true, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'switchable' => true, 'switchable' => true,
@@ -30,6 +30,15 @@ class AssetMaintenancesPresenter extends Presenter
'visible' => true, 'visible' => true,
'formatter' => 'maintenancesLinkFormatter', 'formatter' => 'maintenancesLinkFormatter',
], ],
[
'field' => 'image',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.image'),
'visible' => true,
'formatter' => 'imageFormatter',
],
[ [
'field' => 'company', 'field' => 'company',
'searchable' => true, 'searchable' => true,
@@ -42,7 +51,7 @@ class AssetMaintenancesPresenter extends Presenter
'field' => 'asset_name', 'field' => 'asset_name',
'searchable' => true, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'title' => trans('admin/asset_maintenances/table.asset_name'), 'title' => trans('admin/maintenances/table.asset_name'),
'formatter' => 'assetNameLinkFormatter', 'formatter' => 'assetNameLinkFormatter',
], [ ], [
'field' => 'asset_tag', 'field' => 'asset_tag',
@@ -89,35 +98,35 @@ class AssetMaintenancesPresenter extends Presenter
'field' => 'asset_maintenance_type', 'field' => 'asset_maintenance_type',
'searchable' => true, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'title' => trans('admin/asset_maintenances/form.asset_maintenance_type'), 'title' => trans('admin/maintenances/form.asset_maintenance_type'),
], [ ], [
'field' => 'start_date', 'field' => 'start_date',
'searchable' => true, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'title' => trans('admin/asset_maintenances/form.start_date'), 'title' => trans('admin/maintenances/form.start_date'),
'formatter' => 'dateDisplayFormatter', 'formatter' => 'dateDisplayFormatter',
], [ ], [
'field' => 'completion_date', 'field' => 'completion_date',
'searchable' => true, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'title' => trans('admin/asset_maintenances/form.completion_date'), 'title' => trans('admin/maintenances/form.completion_date'),
'formatter' => 'dateDisplayFormatter', 'formatter' => 'dateDisplayFormatter',
], [ ], [
'field' => 'notes', 'field' => 'notes',
'searchable' => true, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'title' => trans('admin/asset_maintenances/form.notes'), 'title' => trans('admin/maintenances/form.notes'),
], [ ], [
'field' => 'is_warranty', 'field' => 'is_warranty',
'searchable' => true, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'title' => trans('admin/asset_maintenances/table.is_warranty'), 'title' => trans('admin/maintenances/table.is_warranty'),
'formatter' => 'trueFalseFormatter' 'formatter' => 'trueFalseFormatter'
], [ ], [
'field' => 'cost', 'field' => 'cost',
'searchable' => true, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'title' => trans('admin/asset_maintenances/form.cost'), 'title' => trans('admin/maintenances/form.cost'),
'class' => 'text-right', 'class' => 'text-right',
], [ ], [
'field' => 'created_by', 'field' => 'created_by',

View File

@@ -5,7 +5,7 @@ namespace App\Presenters;
/** /**
* Class AccessoryPresenter * Class AccessoryPresenter
*/ */
class UploadsPresenter extends Presenter class UploadedFilesPresenter extends Presenter
{ {
/** /**
* Json Column Layout for bootstrap table * Json Column Layout for bootstrap table
@@ -18,7 +18,7 @@ class UploadsPresenter extends Presenter
$layout = [ $layout = [
[ [
'field' => 'id', 'field' => 'id',
'searchable' => false, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'switchable' => true, 'switchable' => true,
'title' => trans('general.id'), 'title' => trans('general.id'),
@@ -30,6 +30,7 @@ class UploadsPresenter extends Presenter
'sortable' => false, 'sortable' => false,
'switchable' => false, 'switchable' => false,
'title' => trans('general.type'), 'title' => trans('general.type'),
'visible' => true,
'formatter' => 'iconFormatter', 'formatter' => 'iconFormatter',
], ],
[ [
@@ -38,16 +39,17 @@ class UploadsPresenter extends Presenter
'sortable' => false, 'sortable' => false,
'switchable' => true, 'switchable' => true,
'title' => trans('general.image'), 'title' => trans('general.image'),
'formatter' => 'inlineImageFormatter', 'visible' => true,
'formatter' => 'filePreviewFormatter',
], ],
[ [
'field' => 'filename', 'field' => 'filename',
'searchable' => false, 'searchable' => true,
'sortable' => false, 'sortable' => true,
'switchable' => true, 'switchable' => true,
'title' => trans('general.file_name'), 'title' => trans('general.file_name'),
'visible' => true, 'visible' => true,
'formatter' => 'fileUploadNameFormatter', 'formatter' => 'fileNameFormatter',
], ],
[ [
'field' => 'download', 'field' => 'download',
@@ -56,7 +58,7 @@ class UploadsPresenter extends Presenter
'switchable' => true, 'switchable' => true,
'title' => trans('general.download'), 'title' => trans('general.download'),
'visible' => true, 'visible' => true,
'formatter' => 'downloadOrOpenInNewWindowFormatter', 'formatter' => 'fileDownloadButtonsFormatter',
], ],
[ [
'field' => 'note', 'field' => 'note',
@@ -68,10 +70,10 @@ class UploadsPresenter extends Presenter
], ],
[ [
'field' => 'created_by', 'field' => 'created_by',
'searchable' => false, 'searchable' => true,
'sortable' => true, 'sortable' => true,
'title' => trans('general.created_by'), 'title' => trans('general.created_by'),
'visible' => false, 'visible' => true,
'formatter' => 'usersLinkObjFormatter', 'formatter' => 'usersLinkObjFormatter',
], ],
[ [
@@ -80,7 +82,7 @@ class UploadsPresenter extends Presenter
'sortable' => true, 'sortable' => true,
'switchable' => true, 'switchable' => true,
'title' => trans('general.created_at'), 'title' => trans('general.created_at'),
'visible' => false, 'visible' => true,
'formatter' => 'dateDisplayFormatter', 'formatter' => 'dateDisplayFormatter',
], [ ], [
'field' => 'available_actions', 'field' => 'available_actions',
@@ -88,6 +90,7 @@ class UploadsPresenter extends Presenter
'sortable' => false, 'sortable' => false,
'switchable' => false, 'switchable' => false,
'title' => trans('table.actions'), 'title' => trans('table.actions'),
'visible' => true,
'formatter' => 'deleteUploadFormatter', 'formatter' => 'deleteUploadFormatter',
], ],
]; ];

View File

@@ -124,6 +124,15 @@ class UserPresenter extends Presenter
'visible' => true, 'visible' => true,
'formatter' => 'phoneFormatter', 'formatter' => 'phoneFormatter',
], ],
[
'field' => 'mobile',
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('admin/users/table.mobile'),
'visible' => false,
'formatter' => 'mobileFormatter',
],
[ [
'field' => 'website', 'field' => 'website',
'searchable' => true, 'searchable' => true,
@@ -180,7 +189,7 @@ class UserPresenter extends Presenter
'switchable' => false, 'switchable' => false,
'title' => trans('admin/users/table.username'), 'title' => trans('admin/users/table.username'),
'visible' => true, 'visible' => true,
'formatter' => 'usersLinkFormatter', 'formatter' => 'usernameRoleLinkFormatter',
], ],
[ [
'field' => 'employee_num', 'field' => 'employee_num',

View File

@@ -7,6 +7,7 @@ use App\Models\Asset;
use App\Models\Component; use App\Models\Component;
use App\Models\Consumable; use App\Models\Consumable;
use App\Models\License; use App\Models\License;
use App\Models\Maintenance;
use App\Models\User; use App\Models\User;
use App\Models\Setting; use App\Models\Setting;
use App\Models\SnipeSCIMConfig; use App\Models\SnipeSCIMConfig;
@@ -17,6 +18,7 @@ use App\Observers\ComponentObserver;
use App\Observers\ConsumableObserver; use App\Observers\ConsumableObserver;
use App\Observers\LicenseObserver; use App\Observers\LicenseObserver;
use App\Observers\SettingObserver; use App\Observers\SettingObserver;
use App\Observers\MaintenanceObserver;
use Illuminate\Routing\UrlGenerator; use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@@ -67,6 +69,7 @@ class AppServiceProvider extends ServiceProvider
Schema::defaultStringLength(191); Schema::defaultStringLength(191);
Asset::observe(AssetObserver::class); Asset::observe(AssetObserver::class);
Maintenance::observe(MaintenanceObserver::class);
User::observe(UserObserver::class); User::observe(UserObserver::class);
Accessory::observe(AccessoryObserver::class); Accessory::observe(AccessoryObserver::class);
Component::observe(ComponentObserver::class); Component::observe(ComponentObserver::class);

View File

@@ -101,13 +101,21 @@ class AuthServiceProvider extends ServiceProvider
* This is where we set the superadmin permission to allow superadmins to be able to do everything within the system. * This is where we set the superadmin permission to allow superadmins to be able to do everything within the system.
* *
*/ */
Gate::before(function ($user) { Gate::before(function ($user, $ability) {
// Disallow even superadmins to edit non-editable things when in demo mode.
// (We have to do this to prevent jerks from trying to break the demo by editing things they shouldn't.)
if (($ability == 'editableOnDemo') && (config('app.lock_passwords'))) {
return false;
}
if ($user->isSuperUser()) { if ($user->isSuperUser()) {
return true; return true;
} }
}); });
/** /**
* GENERAL GATES * GENERAL GATES
* *
@@ -115,6 +123,45 @@ class AuthServiceProvider extends ServiceProvider
* use in our controllers to determine if a user has access to a certain area. * use in our controllers to determine if a user has access to a certain area.
*/ */
Gate::define('canEditAuthFields', function ($user, $item) {
if ($item instanceof User) {
// if they can only edit users, deny them if the user is admin or superadmin
if (($user->hasAccess('users.edit')) && (!$user->isAdmin()) && (!$user->isAdmin())) {
if ($item->isAdmin() || $item->isSuperUser()) {
return false;
}
return true;
}
// if they are an admin, deny them only if the user is a superadmin
if ($user->hasAccess('admin')) {
if ($item->isSuperUser()) {
return false;
}
return true;
}
return false;
}
return false;
});
/**
* Define the demo mode gate so we have an easy way to use @can and Gate::allows()
*/
Gate::define('editableOnDemo', function () {
if (config('app.lock_passwords')) {
return false;
}
return true;
});
Gate::define('admin', function ($user) { Gate::define('admin', function ($user) {
if ($user->hasAccess('admin')) { if ($user->hasAccess('admin')) {
return true; return true;
@@ -249,5 +296,6 @@ class AuthServiceProvider extends ServiceProvider
return $user->canEditProfile(); return $user->canEditProfile();
}); });
} }
} }

View File

@@ -2,7 +2,7 @@
use App\Models\Accessory; use App\Models\Accessory;
use App\Models\Asset; use App\Models\Asset;
use App\Models\AssetMaintenance; use App\Models\Maintenance;
use App\Models\AssetModel; use App\Models\AssetModel;
use App\Models\Category; use App\Models\Category;
use App\Models\Company; use App\Models\Company;
@@ -413,14 +413,14 @@ class BreadcrumbsServiceProvider extends ServiceProvider
->push(trans('general.create'), route('maintenances.create')) ->push(trans('general.create'), route('maintenances.create'))
); );
Breadcrumbs::for('maintenances.show', fn (Trail $trail, AssetMaintenance $maintenance) => Breadcrumbs::for('maintenances.show', fn (Trail $trail, Maintenance $maintenance) =>
$trail->parent('maintenances.index', route('maintenances.index')) $trail->parent('maintenances.index', route('maintenances.index'))
->push($maintenance->title, route('maintenances.show', $maintenance)) ->push($maintenance->name, route('maintenances.show', $maintenance))
); );
Breadcrumbs::for('maintenances.edit', fn (Trail $trail, AssetMaintenance $maintenance) => Breadcrumbs::for('maintenances.edit', fn (Trail $trail, Maintenance $maintenance) =>
$trail->parent('maintenances.index', route('maintenances.index')) $trail->parent('maintenances.index', route('maintenances.index'))
->push(trans('general.breadcrumb_button_actions.edit_item', ['name' => $maintenance->title]), route('maintenances.edit', $maintenance)) ->push(trans('general.breadcrumb_button_actions.edit_item', ['name' => $maintenance->name]), route('maintenances.edit', $maintenance))
); );
@@ -539,6 +539,26 @@ class BreadcrumbsServiceProvider extends ServiceProvider
->push(trans('general.users'), route('users.index')) ->push(trans('general.users'), route('users.index'))
->push(trans('general.deleted_users'), route('users.index')) ->push(trans('general.deleted_users'), route('users.index'))
); );
} elseif ((request()->is('users*')) && (request()->admins=='true')) {
Breadcrumbs::for('users.index', fn(Trail $trail) => $trail->parent('home', route('home'))
->push(trans('general.users'), route('users.index'))
->push(trans('general.show_admins'), route('users.index'))
);
} elseif ((request()->is('users*')) && (request()->superadmins=='true')) {
Breadcrumbs::for('users.index', fn(Trail $trail) => $trail->parent('home', route('home'))
->push(trans('general.users'), route('users.index'))
->push(trans('general.show_superadmins'), route('users.index'))
);
} elseif ((request()->is('users*')) && (request()->activated=='0')) {
Breadcrumbs::for('users.index', fn(Trail $trail) => $trail->parent('home', route('home'))
->push(trans('general.users'), route('users.index'))
->push(trans('general.login_disabled'), route('users.index'))
);
} elseif ((request()->is('users*')) && (request()->activated=='1')) {
Breadcrumbs::for('users.index', fn(Trail $trail) => $trail->parent('home', route('home'))
->push(trans('general.users'), route('users.index'))
->push(trans('general.login_enabled'), route('users.index'))
);
} else { } else {
Breadcrumbs::for('users.index', fn(Trail $trail) => $trail->parent('home', route('home')) Breadcrumbs::for('users.index', fn(Trail $trail) => $trail->parent('home', route('home'))
->push(trans('general.users'), route('users.index')) ->push(trans('general.users'), route('users.index'))

View File

@@ -31,7 +31,7 @@ class SettingsServiceProvider extends ServiceProvider
// Make sure the limit is actually set, is an integer and does not exceed system limits // Make sure the limit is actually set, is an integer and does not exceed system limits
\App::singleton('api_limit_value', function () { app()->singleton('api_limit_value', function () {
$limit = config('app.max_results'); $limit = config('app.max_results');
$int_limit = intval(request('limit')); $int_limit = intval(request('limit'));
@@ -43,7 +43,7 @@ class SettingsServiceProvider extends ServiceProvider
}); });
// Make sure the offset is actually set and is an integer // Make sure the offset is actually set and is an integer
\App::singleton('api_offset_value', function () { app()->singleton('api_offset_value', function () {
$offset = intval(request('offset')); $offset = intval(request('offset'));
return $offset; return $offset;
}); });
@@ -57,117 +57,121 @@ class SettingsServiceProvider extends ServiceProvider
// Model paths and URLs // Model paths and URLs
\App::singleton('eula_pdf_path', function () { app()->singleton('eula_pdf_path', function () {
return 'eula_pdf_path/'; return 'eula_pdf_path/';
}); });
\App::singleton('assets_upload_path', function () { app()->singleton('assets_upload_path', function () {
return 'assets/'; return 'assets/';
}); });
\App::singleton('audits_upload_path', function () { app()->singleton('maintenances_path', function () {
return 'maintenances/';
});
app()->singleton('audits_upload_path', function () {
return 'audits/'; return 'audits/';
}); });
\App::singleton('accessories_upload_path', function () { app()->singleton('accessories_upload_path', function () {
return 'public/uploads/accessories/'; return 'public/uploads/accessories/';
}); });
\App::singleton('models_upload_path', function () { app()->singleton('models_upload_path', function () {
return 'models/'; return 'models/';
}); });
\App::singleton('models_upload_url', function () { app()->singleton('models_upload_url', function () {
return 'models/'; return 'models/';
}); });
// Categories // Categories
\App::singleton('categories_upload_path', function () { app()->singleton('categories_upload_path', function () {
return 'categories/'; return 'categories/';
}); });
\App::singleton('categories_upload_url', function () { app()->singleton('categories_upload_url', function () {
return 'categories/'; return 'categories/';
}); });
// Locations // Locations
\App::singleton('locations_upload_path', function () { app()->singleton('locations_upload_path', function () {
return 'locations/'; return 'locations/';
}); });
\App::singleton('locations_upload_url', function () { app()->singleton('locations_upload_url', function () {
return 'locations/'; return 'locations/';
}); });
// Users // Users
\App::singleton('users_upload_path', function () { app()->singleton('users_upload_path', function () {
return 'avatars/'; return 'avatars/';
}); });
\App::singleton('users_upload_url', function () { app()->singleton('users_upload_url', function () {
return 'users/'; return 'users/';
}); });
// Manufacturers // Manufacturers
\App::singleton('manufacturers_upload_path', function () { app()->singleton('manufacturers_upload_path', function () {
return 'manufacturers/'; return 'manufacturers/';
}); });
\App::singleton('manufacturers_upload_url', function () { app()->singleton('manufacturers_upload_url', function () {
return 'manufacturers/'; return 'manufacturers/';
}); });
// Suppliers // Suppliers
\App::singleton('suppliers_upload_path', function () { app()->singleton('suppliers_upload_path', function () {
return 'suppliers/'; return 'suppliers/';
}); });
\App::singleton('suppliers_upload_url', function () { app()->singleton('suppliers_upload_url', function () {
return 'suppliers/'; return 'suppliers/';
}); });
// Departments // Departments
\App::singleton('departments_upload_path', function () { app()->singleton('departments_upload_path', function () {
return 'departments/'; return 'departments/';
}); });
\App::singleton('departments_upload_url', function () { app()->singleton('departments_upload_url', function () {
return 'departments/'; return 'departments/';
}); });
// Company paths and URLs // Company paths and URLs
\App::singleton('companies_upload_path', function () { app()->singleton('companies_upload_path', function () {
return 'companies/'; return 'companies/';
}); });
\App::singleton('companies_upload_url', function () { app()->singleton('companies_upload_url', function () {
return 'companies/'; return 'companies/';
}); });
// Accessories paths and URLs // Accessories paths and URLs
\App::singleton('accessories_upload_path', function () { app()->singleton('accessories_upload_path', function () {
return 'accessories/'; return 'accessories/';
}); });
\App::singleton('accessories_upload_url', function () { app()->singleton('accessories_upload_url', function () {
return 'accessories/'; return 'accessories/';
}); });
// Consumables paths and URLs // Consumables paths and URLs
\App::singleton('consumables_upload_path', function () { app()->singleton('consumables_upload_path', function () {
return 'consumables/'; return 'consumables/';
}); });
\App::singleton('consumables_upload_url', function () { app()->singleton('consumables_upload_url', function () {
return 'consumables/'; return 'consumables/';
}); });
// Components paths and URLs // Components paths and URLs
\App::singleton('components_upload_path', function () { app()->singleton('components_upload_path', function () {
return 'components/'; return 'components/';
}); });
\App::singleton('components_upload_url', function () { app()->singleton('components_upload_url', function () {
return 'components/'; return 'components/';
}); });

View File

@@ -5,9 +5,11 @@ namespace App\Rules;
use Closure; use Closure;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Crypt;
use Illuminate\Validation\Concerns\ValidatesAttributes;
class AlphaEncrypted implements ValidationRule class AlphaEncrypted implements ValidationRule
{ {
use ValidatesAttributes;
/** /**
* Run the validation rule. * Run the validation rule.
* *
@@ -18,7 +20,7 @@ class AlphaEncrypted implements ValidationRule
try { try {
$attributeName = trim(preg_replace('/_+|snipeit|\d+/', ' ', $attribute)); $attributeName = trim(preg_replace('/_+|snipeit|\d+/', ' ', $attribute));
$decrypted = Crypt::decrypt($value); $decrypted = Crypt::decrypt($value);
if (!ctype_alpha($decrypted) && !is_null($decrypted)) { if (!$this->validateAlpha($attributeName, $decrypted, 'ascii') && !is_null($decrypted)) {
$fail(trans('validation.alpha', ['attribute' => $attributeName])); $fail(trans('validation.alpha', ['attribute' => $attributeName]));
} }
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Validation\Concerns\ValidatesAttributes;
class BooleanEncrypted implements ValidationRule
{
use ValidatesAttributes;
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$attributeName = trim(preg_replace('/_+|snipeit|\d+/', ' ', $attribute));
$decrypted = Crypt::decrypt($value);
if (!$this->validateBoolean($attributeName, $decrypted) && !is_null($decrypted)) {
$fail(trans('validation.ipv6', ['attribute' => $attributeName]));
}
} catch (\Exception $e) {
report($e);
$fail(trans('general.something_went_wrong'));
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Validation\Concerns\ValidatesAttributes;
class DateEncrypted implements ValidationRule
{
use ValidatesAttributes;
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$attributeName = trim(preg_replace('/_+|snipeit|\d+/', ' ', $attribute));
$decrypted = Crypt::decrypt($value);
if (!$this->validateDate($attributeName, $decrypted) && !is_null($decrypted)) {
$fail(trans('validation.date', ['attribute' => $attributeName]));
}
} catch (\Exception $e) {
report($e);
$fail(trans('general.something_went_wrong'));
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Validation\Concerns\ValidatesAttributes;
class EmailEncrypted implements ValidationRule
{
use ValidatesAttributes;
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$attributeName = trim(preg_replace('/_+|snipeit|\d+/', ' ', $attribute));
$decrypted = Crypt::decrypt($value);
if (!$this->validateEmail($attribute, $decrypted, []) && !is_null($decrypted)) {
$fail(trans('validation.email', ['attribute' => $attributeName]));
}
} catch (\Exception $e) {
report($e);
$fail(trans('general.something_went_wrong'));
}
}
}

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