Compare commits

...

263 Commits

Author SHA1 Message Date
snipe cbae5ce9ab Updated config - needs testing
Signed-off-by: snipe <snipe@snipe.net>
2025-08-06 15:52:07 +01:00
snipe be3c8ddd5c Hotfix for FD-50160
Signed-off-by: snipe <snipe@snipe.net>
2025-08-05 23:19:27 +01: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
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
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 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
Godfrey M fc469707a3 clean up 2025-07-16 10:51:33 -07:00
snipe 77fdc370c7 Merge pull request #17415 from uberbrady/clean_unaccepted_assets_report
[FD-47386, FD-49095] New Artisan command to clean checkout acceptances
2025-07-16 17:34:49 +01:00
snipe 301290fb6d Send emails on acceptance even if signature is not required
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 17:02:04 +01:00
snipe 07fffe2f79 Merge pull request #17410 from grokability/remove-password-from-welcome
Remove password from welcome email, prompt for reset instead
2025-07-16 16:54:07 +01:00
snipe 0227a63fa5 Slightly clearer language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:31:45 +01:00
snipe 27764b863c Updated language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:25:36 +01:00
snipe 032fd75f9e Added default invite password token timeout
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:23:51 +01:00
snipe 0bf4f861f3 Nicer debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:23:25 +01:00
snipe fd8f90cb52 Added new password broker for longer toekn lifetime
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:23:11 +01:00
snipe b6c6b025c8 Added expiry language
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:20:26 +01:00
snipe 3d89e98d1f Small tweaks to welcome email blade
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 16:20:15 +01:00
Brady Wetherington 7c5110ed5d Add more action logs tests everywhere I can think of it. 2025-07-16 16:20:06 +01:00
Brady Wetherington 0a474f48ad WIP: Adding various tests of the contents of ActionLogs for lots of events 2025-07-16 16:20:06 +01:00
Brady Wetherington c409bfd5be New Artisan command to clean checkout acceptances and a migration that runs it 2025-07-16 16:06:23 +01:00
snipe 39d5d5b2e0 Merge branch 'develop' into remove-password-from-welcome 2025-07-16 15:05:13 +01:00
snipe 8a80d9009d Refomatted hidden array
Signed-off-by: snipe <snipe@snipe.net>
2025-07-16 12:24:48 +01:00
Godfrey M f62b5df566 use ternaries instead of optionals 2025-07-15 15:40:21 -07:00
spencerrlongg 826521f053 added rules, still needs a little more... 2025-07-15 15:21:10 -05:00
spencerrlongg f9b05bc8de more encryption rules extenting laravel's own 2025-07-15 15:03:51 -05:00
spencerrlongg b8239e8ed9 use laravel validation methods, email works 2025-07-15 14:17:49 -05:00
Godfrey M 214757ab0b fix mailable 2025-07-15 12:04:36 -07:00
Godfrey M f130186b37 add Component Checkin Mail 2025-07-15 11:56:34 -07:00
Godfrey M 2244eebc3b add Component Checkout Mail 2025-07-15 11:00:39 -07:00
snipe 4176792f2d Translate field
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 16:32:17 +01:00
snipe 1e6cef52c9 Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 15:17:08 +01:00
snipe a0f4f30a50 Added try/catch
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 15:13:33 +01:00
snipe 4cbf6ac393 Re-add /setup crential email
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:20:13 +01:00
snipe af7425d8e6 Remove unused variable
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:19:12 +01:00
snipe 3fea909d3f Removed send credentials option from user controller
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:14:10 +01:00
snipe 7c37d40677 Use plaintext in the database so that the password will never be valid
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:13:50 +01:00
snipe 3a97c27350 Removed logging
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:13:29 +01:00
snipe e0516a52a8 Formatting change
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:55 +01:00
snipe a85ec6efb3 Set token in welcome email constructor
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:42 +01:00
snipe 3795c74814 Added string
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:26 +01:00
snipe 27954dc6d3 Use password reset token
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:12:18 +01:00
snipe 68c4187a09 Removed email creds option from user create
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:11:15 +01:00
snipe b9834231f3 Remove email credentials chexkbox
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 14:08:36 +01:00
snipe 2be343ea1c More specific no password
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 13:11:45 +01:00
snipe 109fe1b62c Use no password as temp password
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 13:11:18 +01:00
snipe 63d691a63c Removed noisy log
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 13:10:48 +01:00
snipe 6f57d6b876 Merge pull request #17407 from grokability/fixes-signature-pad-chrome
Fixed display of acceptance button if signature is not required
2025-07-15 10:58:34 +01:00
snipe e0bad99ea1 Fixes display of acceptannce button if signature is not required
Signed-off-by: snipe <snipe@snipe.net>
2025-07-15 10:55:30 +01:00
snipe e39eb09cfb Merge pull request #17390 from Godmartinz/unhandled-redirect-error
FIXED redirect option being NULL
2025-07-10 19:41:40 +01:00
Godfrey M 64d397c3f3 add component notification tests 2025-07-10 11:26:10 -07:00
Godfrey M 465ac1d1e1 remove ternary 2025-07-10 08:39:13 -07:00
Godfrey M 18d6becebc populate other_redirect in store method 2025-07-10 08:36:15 -07:00
Godfrey M 3bbd0fdbcd google notifications fires properly 2025-07-09 17:02:51 -07:00
Godfrey M 8214b11da5 MS teams fires properly 2025-07-09 11:44:53 -07:00
Godfrey M 36090bf83e checked in notification fires, updated icon translation usage 2025-07-09 11:35:24 -07:00
Godfrey M bffb2fe82f checkout notification fires 2025-07-09 11:23:27 -07:00
Godfrey M 500cbf5d92 add component checkout notification, update checkout blade, update listener 2025-07-09 11:12:18 -07:00
Godfrey M c8b213c190 remove some changes, move error bag 2025-06-24 13:10:32 -07:00
Godfrey M 942de9dce5 got validation to redirect back to form and display 2025-06-24 12:42:07 -07:00
1109 changed files with 25309 additions and 310420 deletions
+2
View File
@@ -28,6 +28,7 @@ PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=db
DB_SOCKET=null
DB_PORT='3306'
DB_DATABASE=snipeit
DB_USERNAME=snipeit
@@ -168,6 +169,7 @@ AWS_DEFAULT_REGION=null
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60
RESET_PASSWORD_LINK_EXPIRES=900
INVITE_PASSWORD_LINK_EXPIRES=1500
# --------------------------------------------
# OPTIONAL: MISC
+2
View File
@@ -24,6 +24,7 @@ PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_SOCKET=null
DB_PORT=3306
DB_DATABASE=null
DB_USERNAME=null
@@ -174,6 +175,7 @@ LOGIN_AUTOCOMPLETE=false
RESET_PASSWORD_LINK_EXPIRES=15
PASSWORD_CONFIRM_TIMEOUT=10800
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
INVITE_PASSWORD_LINK_EXPIRES=1500
# --------------------------------------------
# OPTIONAL: MISC
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@v4.4.5
uses: codacy/codacy-analysis-cli-action@v4.4.7
with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations
@@ -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");
}
}
+10 -1
View File
@@ -133,8 +133,17 @@ class Handler extends ExceptionHandler
// This is traaaaash but it handles models that are not found while using route model binding :(
// The only alternative is to set that at *each* route, which is crazypants
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
$ids = method_exists($e, 'getIds') ? $e->getIds() : [];
// This gets the MVC model name from the exception and formats in a way that's less fugly
if (in_array('bulkedit', $ids, true)) {
$error_array = session()->get('bulk_asset_errors');
return redirect()
->route('hardware.bulkedit')
->withErrors($error_array, 'bulk_asset_errors')
->withInput();
}
// This gets the MVC model name from the exception and formats in a way that's less fugly
$model_name = strtolower(implode(" ", preg_split('/(?=[A-Z])/', last(explode('\\', $e->getModel())))));
$route = str_plural(strtolower(last(explode('\\', $e->getModel())))).'.index';
+1
View File
@@ -151,6 +151,7 @@ class IconHelper
case 'location':
return 'fas fa-map-marker-alt';
case 'superadmin':
case 'admin':
return 'fas fa-crown';
case 'print':
return 'fa-solid fa-print';
+30 -1
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
@@ -52,7 +82,6 @@ class StorageHelper
'pdf',
'png',
'svg',
'svg',
'wav',
'webm',
'webp',
@@ -77,7 +77,18 @@ class AccessoriesController extends Controller
$accessory->supplier_id = request('supplier_id');
$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);
}
session()->put(['redirect_option' => $request->get('redirect_option')]);
// Was the accessory created?
@@ -114,11 +125,12 @@ class AccessoriesController extends Controller
$this->authorize('create', Accessory::class);
$cloned = clone $accessory;
$accessory_to_clone = $accessory;
$cloned->id = null;
$cloned->deleted_at = '';
$cloned->location_id = null;
return view('accessories/edit')
->with('cloned_model', $accessory_to_clone)
->with('item', $cloned);
}
@@ -93,19 +93,27 @@ class UploadedFilesController extends Controller
'id',
'filename',
'action_type',
'action_date',
'note',
'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');
$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
// We could use the normal Actionlogs text scope, but it's a very heavy query since it's searcghing across all relations
// And we generally won't need that here
// We could use the normal Actionlogs text scope, but it's a very heavy query since it's searching across all relations
// and we generally won't need that here
if ($request->filled('search')) {
$uploads->where(
@@ -116,8 +124,10 @@ class UploadedFilesController extends Controller
);
}
$total = $uploads->count();
$uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get();
return (new UploadedFilesTransformer())->transformFiles($uploads, $uploads->count());
return (new UploadedFilesTransformer())->transformFiles($uploads, $total);
}
+34 -3
View File
@@ -23,6 +23,7 @@ use App\Notifications\CurrentInventory;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
@@ -81,7 +82,12 @@ class UsersController extends Controller
'users.autoassign_licenses',
'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([
'assets as assets_count' => function(Builder $query) {
$query->withoutTrashed();
@@ -102,6 +108,14 @@ class UsersController extends Controller
$users = $users->where('users.activated', '=', $request->input('activated'));
}
if ($request->input('admins') == 'true') {
$users = $users->OnlyAdminsAndSuperAdmins();
}
if ($request->input('superadmins') == 'true') {
$users = $users->OnlySuperAdmins();
}
if ($request->filled('company_id')) {
$users = $users->where('users.company_id', '=', $request->input('company_id'));
}
@@ -475,8 +489,25 @@ class UsersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
}
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
// check for permissions related fields and pull them out if the current user cannot edit them
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
if ($request->filled('username')) {
$user->username = $request->input('username');
}
if ($request->filled('email')) {
$user->email = $request->input('email');
}
if ($request->filled('activated')) {
$user->activated = $request->input('activated');
}
}
// We need to use has() instead of filled()
+15 -2
View File
@@ -87,7 +87,20 @@ class AssetModelsController extends Controller
$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 ($this->shouldAddDefaultValues($request->input())) {
@@ -271,7 +284,7 @@ class AssetModelsController extends Controller
->with('depreciation_list', Helper::depreciationList())
->with('item', $model)
->with('model_id', $model->id)
->with('clone_model', $cloned_model);
->with('cloned_model', $cloned_model);
}
@@ -157,8 +157,16 @@ class AssetsController extends Controller
$asset->location_id = $request->input('rtd_location_id', null);
}
// Create the image (if one was chosen.)
if ($request->has('image')) {
if ($request->has('use_cloned_image')) {
$cloned_model_img = Asset::select('image')->find($request->input('clone_image_from_id'));
if ($cloned_model_img) {
$new_image_name = 'clone-'.date('U').'-'.$cloned_model_img->image;
$new_image = 'assets/'.$new_image_name;
Storage::disk('public')->copy('assets/'.$cloned_model_img->image, $new_image);
$asset->image = $new_image_name;
}
} else {
$asset = $request->handleImages($asset);
}
@@ -227,7 +235,10 @@ class AssetsController extends Controller
}
}
session()->put(['redirect_option' => $request->get('redirect_option'), 'checkout_to_type' => $request->get('checkout_to_type')]);
session()->put(['redirect_option' => $request->get('redirect_option'),
'checkout_to_type' => $request->get('checkout_to_type'),
'other_redirect' => 'model' ]);
if ($successes) {
@@ -641,8 +652,9 @@ class AssetsController extends Controller
*/
public function getClone(Asset $asset)
{
$this->authorize('create', $asset);
$this->authorize('create', Asset::class);
$cloned = clone $asset;
$cloned_model = $asset;
$cloned->id = null;
$cloned->asset_tag = '';
$cloned->serial = '';
@@ -652,6 +664,7 @@ class AssetsController extends Controller
return view('hardware/edit')
->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList())
->with('cloned_model', $cloned_model)
->with('item', $cloned);
}
@@ -161,6 +161,7 @@ class BulkAssetsController extends Controller
$models = $assets->unique('model_id');
$modelNames = [];
foreach($models as $model) {
$modelNames[] = $model->model->name;
}
@@ -196,7 +197,6 @@ class BulkAssetsController extends Controller
case 'edit':
$this->authorize('update', Asset::class);
return view('hardware/bulk')
->with('assets', $asset_ids)
->with('statuslabel_list', Helper::statusLabelList())
@@ -224,11 +224,8 @@ class BulkAssetsController extends Controller
$error_array = array();
// Get the back url from the session and then destroy the session
$bulk_back_url = route('hardware.index');
if ($request->session()->has('bulk_back_url')) {
$bulk_back_url = $request->session()->pull('bulk_back_url');
}
$bulk_back_url = $request->session()->pull('bulk_back_url', url()->previous());
$custom_field_columns = CustomField::all()->pluck('db_column')->toArray();
@@ -543,7 +540,13 @@ class BulkAssetsController extends Controller
} // end asset foreach
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.bulkedit')
->with('bulk_asset_errors', $error_array)
->withInput();
}
return redirect($bulk_back_url)->with('success', trans('admin/hardware/message.update.success'));
@@ -735,4 +738,33 @@ class BulkAssetsController extends Controller
}
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);
}
}
@@ -7,7 +7,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Company;
use App\Models\Consumable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Http\RedirectResponse;
use \Illuminate\Contracts\View\View;
@@ -81,11 +81,22 @@ class ConsumablesController extends Controller
$consumable->purchase_date = $request->input('purchase_date');
$consumable->purchase_cost = $request->input('purchase_cost');
$consumable->qty = $request->input('qty');
$consumable->created_by = auth()->id();
$consumable->created_by = auth()->id();
$consumable->notes = $request->input('notes');
$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);
}
session()->put(['redirect_option' => $request->get('redirect_option')]);
@@ -213,9 +224,10 @@ class ConsumablesController extends Controller
$consumable_to_close = $consumable;
$consumable = clone $consumable_to_close;
$consumable->id = null;
$consumable->image = null;
$consumable->created_by = null;
return view('consumables/edit')->with('item', $consumable);
return view('consumables/edit')
->with('cloned_model', $consumable_to_close)
->with('item', $consumable);
}
}
@@ -304,13 +304,16 @@ class LicensesController extends Controller
$response = new StreamedResponse(function () {
// Open output stream
$handle = fopen('php://output', 'w');
$licenses= License::with('company',
$licenses = License::with('company',
'manufacturer',
'category',
'supplier',
'adminuser',
'assignedusers')
->orderBy('created_at', 'DESC');
'assignedusers');
if (request()->filled('category_id')) {
$licenses = $licenses->where('category_id', request()->input('category_id'));
}
$licenses = $licenses->orderBy('created_at', 'DESC');
Company::scopeCompanyables($licenses)
->chunk(500, function ($licenses) use ($handle) {
$headers = [
+13 -2
View File
@@ -96,7 +96,18 @@ class LocationsController extends Controller
$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()) {
return redirect()->route('locations.index')->with('success', trans('admin/locations/message.create.success'));
@@ -275,9 +286,9 @@ class LocationsController extends Controller
// unset these values
$location->id = null;
$location->image = null;
return view('locations/edit')
->with('cloned_model', $location_to_clone)
->with('item', $location);
}
@@ -51,7 +51,7 @@ class ManufacturersController extends Controller
$manufacturers_count = Manufacturer::withTrashed()->count();
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'));
}
@@ -25,32 +25,22 @@ class UserFilesController extends Controller
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);
if ($request->hasFile('file')) {
if (! Storage::exists('private_uploads/users')) {
Storage::makeDirectory('private_uploads/users', 775);
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.upload.success'));
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/users/','user-'.$user->id, $file);
$user->logUpload($file_name, $request->get('notes'));
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.upload.success'));
}
return redirect()->back()->with('error', trans('admin/users/message.upload.nofiles'));
}
+37 -43
View File
@@ -13,15 +13,8 @@ use App\Models\Company;
use App\Models\Group;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Storage;
use Redirect;
use Str;
use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Notifications\CurrentInventory;
@@ -130,7 +123,7 @@ class UsersController extends Controller
}
$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');
session()->put(['redirect_option' => $request->get('redirect_option')]);
@@ -142,18 +135,6 @@ class UsersController extends Controller
$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')
->with('success', trans('admin/users/message.success.create'));
}
@@ -248,20 +229,14 @@ 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
$user->username = trim($request->input('username'));
$user->email = trim($request->input('email'));
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone');
$user->location_id = $request->input('location_id', null);
@@ -273,8 +248,6 @@ class UsersController extends Controller
$user->city = $request->input('city', null);
$user->state = $request->input('state', 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->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0);
@@ -283,30 +256,49 @@ class UsersController extends Controller
$user->end_date = $request->input('end_date', null);
$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
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
->update(['location_id' => $request->input('location_id', null)]);
// Do we want to update the user password?
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
// check for permissions related fields and only set them if the user has permission to edit them
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
$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
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->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
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
@@ -440,7 +432,7 @@ class UsersController extends Controller
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
$this->authorize('view', $user_to_clone);
@@ -455,6 +447,8 @@ class UsersController extends Controller
$user->last_name = '';
$user->email = substr($user->email, ($pos = strpos($user->email, '@')) !== false ? $pos : 0);
$user->id = null;
$user->username = null;
$user->avatar = null;
// Get this user's groups
$userGroups = $user_to_clone->groups()->pluck('name', 'id');
@@ -470,7 +464,7 @@ class UsersController extends Controller
->with('user', $user)
->with('groups', Group::pluck('name', 'id'))
->with('userGroups', $userGroups)
->with('clone_user', $user_to_clone)
->with('cloned_model', $user_to_clone)
->with('item', $user);
}
+13 -2
View File
@@ -80,9 +80,20 @@ class UploadFileRequest extends Request
{
$attributes = [];
if ($this->file) {
if (($this->file) && (is_array($this->file))) {
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';
}
}
}
@@ -2,6 +2,7 @@
namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CustomField;
@@ -16,6 +17,7 @@ use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class ActionlogsTransformer
{
@@ -133,24 +135,6 @@ class ActionlogsTransformer
$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 = [
'id' => (int) $actionlog->id,
@@ -158,8 +142,10 @@ class ActionlogsTransformer
'file' => ($actionlog->filename!='')
?
[
'url' => $file_url,
'url' => $actionlog->uploads_file_url(),
'filename' => $actionlog->filename,
'inlineable' => StorageHelper::allowSafeInline($actionlog->uploads_file_url()),
'exists_on_disk' => Storage::exists($actionlog->uploads_file_path()) ? true : false,
] : null,
'item' => ($actionlog->item) ? [
+1 -1
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,
'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),
'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_expires' => ($asset->warranty_months > 0) ? Helper::getFormattedDateObject($asset->warranty_expires, 'date') : null,
'created_by' => ($asset->adminuser) ? [
@@ -204,6 +203,7 @@ class AssetsTransformer
'last_name'=> ($asset->assigned->last_name) ? e($asset->assigned->last_name) : null,
'email'=> ($asset->assigned->email) ? e($asset->assigned->email) : null,
'employee_number' => ($asset->assigned->employee_num) ? e($asset->assigned->employee_num) : null,
'jobtitle' => $asset->assigned->jobtitle ? e($asset->assigned->jobtitle) : null,
'type' => 'user',
] : null;
}
@@ -62,7 +62,7 @@ class LicensesTransformer
'checkin' => Gate::allows('checkin', License::class),
'clone' => Gate::allows('create', 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;
@@ -32,10 +32,11 @@ class UploadedFilesTransformer
'name' => e($file->filename),
'item' => ($file->item_type) ? [
'id' => (int) $file->item_id,
'type' => strtolower(class_basename($file->item_type)),
'type' => str_plural(strtolower(class_basename($file->item_type))),
] : null,
'filename' => e($file->filename),
'filetype' => StorageHelper::getFiletype($file->uploads_file_path()),
'mediatype' => StorageHelper::getMediaType($file->uploads_file_path()),
'url' => $file->uploads_file_url(),
'note' => ($file->note) ? e($file->note) : null,
'created_by' => ($file->adminuser) ? [
@@ -44,7 +45,7 @@ class UploadedFilesTransformer
] : null,
'created_at' => Helper::getFormattedDateObject($file->created_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),
];
@@ -22,6 +22,12 @@ class UsersTransformer
public function transformUser(User $user)
{
$role = '';
if ($user->isSuperUser()) {
$role = 'superadmin';
} elseif ($user->isAdmin()) {
$role = 'admin';
}
$array = [
'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar) ?? null,
@@ -59,6 +65,7 @@ class UsersTransformer
'name'=> e($user->userloc->name),
] : null,
'notes'=> Helper::parseEscapedMarkedownInline($user->notes),
'role' => $role,
'permissions' => $user->decodePermissions(),
'activated' => ($user->activated == '1') ? true : false,
'autoassign_licenses' => ($user->autoassign_licenses == '1') ? true : false,
+1 -1
View File
@@ -133,7 +133,7 @@ abstract class Importer
} else {
$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
+30 -10
View File
@@ -7,7 +7,9 @@ use App\Models\Department;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
/**
* This is ONLY used for the User Import. When we are importing users
@@ -80,6 +82,7 @@ class UserImporter extends ItemImporter
$this->item['username'] = $user_formatted_array['username'];
}
// 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'])))) {
$user = User::find($this->item['id']);
@@ -89,12 +92,25 @@ class UserImporter extends ItemImporter
if ($user) {
// If the user does not want to update existing values, only add new ones, bail out
if (! $this->updating) {
Log::debug('A matching User '.$this->item['name'].' already exists. ');
return;
}
$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));
// Why do we have to do this twice? Update should
$user->save();
// Update the location of any assets checked out to this user
@@ -110,28 +126,32 @@ class UserImporter extends ItemImporter
// This needs to be applied after the update logic, otherwise we'll overwrite user passwords
// Issue #5408
$this->item['password'] = bcrypt($this->tempPassword);
$this->item['password'] = $this->tempPassword;
$this->log('No matching user, creating one');
$user = new User();
$user->created_by = auth()->id();
$user->fill($this->sanitizeItemForStoring($user));
// TODO - check for gate here I guess
if ($user->save()) {
$this->log('User '.$this->item['name'].' was created');
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) {
$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;
$this->item = null;
@@ -140,9 +160,9 @@ class UserImporter extends ItemImporter
}
$this->logError($user, 'User');
return;
}
/**
* Fetch an existing department, or create new if it doesn't exist
*
+15 -4
View File
@@ -4,10 +4,12 @@ namespace App\Listeners;
use App\Events\CheckoutableCheckedOut;
use App\Mail\CheckinAccessoryMail;
use App\Mail\CheckinComponentMail;
use App\Mail\CheckinLicenseMail;
use App\Mail\CheckoutAccessoryMail;
use App\Mail\CheckoutAssetMail;
use App\Mail\CheckinAssetMail;
use App\Mail\CheckoutComponentMail;
use App\Mail\CheckoutConsumableMail;
use App\Mail\CheckoutLicenseMail;
use App\Models\Accessory;
@@ -22,9 +24,11 @@ use App\Models\Setting;
use App\Models\User;
use App\Notifications\CheckinAccessoryNotification;
use App\Notifications\CheckinAssetNotification;
use App\Notifications\CheckinComponentNotification;
use App\Notifications\CheckinLicenseSeatNotification;
use App\Notifications\CheckoutAccessoryNotification;
use App\Notifications\CheckoutAssetNotification;
use App\Notifications\CheckoutComponentNotification;
use App\Notifications\CheckoutConsumableNotification;
use App\Notifications\CheckoutLicenseSeatNotification;
use GuzzleHttp\Exception\ClientException;
@@ -39,7 +43,7 @@ use Osama\LaravelTeamsNotification\TeamsNotification;
class CheckoutableListener
{
private array $skipNotificationsFor = [
Component::class,
// Component::class,
];
/**
@@ -145,7 +149,6 @@ class CheckoutableListener
$shouldSendEmailToUser = $this->checkoutableCategoryShouldSendEmail($event->checkoutable);
$shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress();
$shouldSendWebhookNotification = $this->shouldSendWebhookNotification();
if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) {
return;
}
@@ -269,6 +272,9 @@ class CheckoutableListener
case LicenseSeat::class:
$notificationClass = CheckinLicenseSeatNotification::class;
break;
case Component::class:
$notificationClass = CheckinComponentNotification::class;
break;
}
Log::debug('Notification class: '.$notificationClass);
@@ -299,6 +305,9 @@ class CheckoutableListener
case LicenseSeat::class:
$notificationClass = CheckoutLicenseSeatNotification::class;
break;
case Component::class:
$notificationClass = CheckoutComponentNotification::class;
break;
}
@@ -310,6 +319,7 @@ class CheckoutableListener
Asset::class => CheckoutAssetMail::class,
LicenseSeat::class => CheckoutLicenseMail::class,
Consumable::class => CheckoutConsumableMail::class,
Component::class => CheckoutComponentMail::class,
];
$mailable= $lookup[get_class($event->checkoutable)];
@@ -322,8 +332,8 @@ class CheckoutableListener
Accessory::class => CheckinAccessoryMail::class,
Asset::class => CheckinAssetMail::class,
LicenseSeat::class => CheckinLicenseMail::class,
Component::class => CheckinComponentMail::class,
];
$mailable= $lookup[get_class($event->checkoutable)];
return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note);
@@ -469,7 +479,8 @@ class CheckoutableListener
return match (true) {
$checkoutable instanceof Asset => $checkoutable->model->category,
$checkoutable instanceof Accessory,
$checkoutable instanceof Consumable => $checkoutable->category,
$checkoutable instanceof Consumable,
$checkoutable instanceof Component => $checkoutable->category,
$checkoutable instanceof LicenseSeat => $checkoutable->license->category,
};
}
+71
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 [];
}
}
+82
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 [];
}
}
+12 -1
View File
@@ -74,6 +74,8 @@ class Actionlog extends SnipeModel
'assets' => ['asset_tag','name', 'serial', 'order_number', 'notes', 'purchase_date'],
'assets.model' => ['name', 'model_number', 'eol', 'notes'],
'assets.model.category' => ['name', 'notes'],
'assets.location' => ['name'],
'assets.defaultLoc' => ['name'],
'assets.model.manufacturer' => ['name', 'notes'],
'licenses' => ['name', 'serial', 'notes', 'order_number', 'license_email', 'license_name', 'purchase_order', 'purchase_date'],
'licenses.category' => ['name', 'notes'],
@@ -455,6 +457,11 @@ class Actionlog extends SnipeModel
public function uploads_file_url()
{
if (($this->action_type == 'accepted') || ($this->action_type == 'declined')) {
return route('log.storedeula.download', ['filename' => $this->filename]);
}
switch ($this->item_type) {
case Accessory::class:
return route('show.accessoryfile', [$this->item_id, $this->id]);
@@ -463,7 +470,7 @@ class Actionlog extends SnipeModel
case AssetModel::class:
return route('show/modelfile', [$this->item_id, $this->id]);
case Consumable::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
return route('show.consumablefile', [$this->item_id, $this->id]);
case Component::class:
return route('show.componentfile', [$this->item_id, $this->id]);
case License::class:
@@ -480,6 +487,10 @@ class Actionlog extends SnipeModel
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) {
case Accessory::class:
return 'private_uploads/accessories/'.$this->filename;
+30 -2
View File
@@ -15,6 +15,8 @@ use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -224,7 +226,11 @@ class Asset extends Depreciable
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);
}
}
@@ -421,12 +427,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
if (($this->model) && ($this->model->fieldset) && ($this->model->fieldset->fields)) {
foreach ($this->model->fieldset->fields as $field) {
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);
}
}
}
}
}
}
+45
View File
@@ -2,12 +2,14 @@
namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\HasUploads;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
/**
@@ -203,6 +205,36 @@ class Component extends SnipeModel
{
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
@@ -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
+1
View File
@@ -16,6 +16,7 @@ class CustomField extends Model
UniqueUndeletedTrait;
/**
*
* Custom field predfined formats
*
* @var array
+66 -12
View File
@@ -3,12 +3,19 @@
namespace App\Models;
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\RegexEncrypted;
use App\Rules\UrlEncrypted;
use Gate;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Watson\Validating\ValidatingTrait;
class CustomFieldset extends Model
@@ -99,7 +106,7 @@ class CustomFieldset extends Model
* @since [v3.0]
* @return array
*/
public function validation_rules()
public function validation_rules(): array
{
$rules = [];
foreach ($this->fields as $field) {
@@ -121,18 +128,65 @@ class CustomFieldset extends Model
$rules[$field->db_column_name()] = $rule;
// these are to replace the standard 'numeric' and 'alpha' rules if the custom field is also encrypted.
// the values need to be decrypted first, because encrypted strings are alphanumeric
if ($field->format === 'NUMERIC' && $field->field_encrypted) {
// this is to switch the rules to rules specially made for encrypted custom fields that decrypt the value before validating
if ($field->field_encrypted) {
$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
if ($field->element != 'checkbox') {
+85 -15
View File
@@ -23,6 +23,7 @@ use Illuminate\Support\Str;
use Laravel\Passport\HasApiTokens;
use Watson\Validating\ValidatingTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
use App\Presenters\UserPresenter;
class User extends SnipeModel implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, HasLocalePreference
{
@@ -30,7 +31,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
use CompanyableTrait;
use HasUploads;
protected $presenter = \App\Presenters\UserPresenter::class;
protected $presenter = UserPresenter::class;
use SoftDeletes, ValidatingTrait, Loggable;
use Authenticatable, Authorizable, CanResetPassword, HasApiTokens;
use UniqueUndeletedTrait;
@@ -38,7 +39,16 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
use Presentable;
use Searchable;
protected $hidden = ['password', 'remember_token', 'permissions', 'reset_password_code', 'persist_code'];
protected $hidden = [
'password',
'remember_token',
'permissions',
'reset_password_code',
'persist_code',
'two_factor_secret',
'activation_code',
];
protected $table = 'users';
protected $injectUniqueIdentifier = true;
@@ -193,11 +203,19 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
{
$user_groups = $this->groups;
if (($this->permissions == '') && (count($user_groups) == 0)) {
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);
//If the user is explicitly granted, return true
@@ -251,6 +269,18 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
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
@@ -278,13 +308,15 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
*/
public function isDeletable()
{
return Gate::allows('delete', $this)
&& ($this->assets->count() === 0)
&& ($this->licenses->count() === 0)
&& ($this->consumables->count() === 0)
&& ($this->accessories->count() === 0)
&& ($this->managedLocations->count() === 0)
&& ($this->managesUsers->count() === 0)
&& (($this->assets_count ?? $this->assets()->count()) === 0)
&& (($this->accessories_count ?? $this->accessories()->count()) === 0)
&& (($this->licenses_count ?? $this->licenses()->count()) === 0)
&& (($this->consumables_count ?? $this->consumables()->count()) === 0)
&& (($this->accessories_count ?? $this->accessories()->count()) === 0)
&& (($this->manages_users_count ?? $this->managesUsers()->count()) === 0)
&& (($this->manages_locations_count ?? $this->managedLocations()->count()) === 0)
&& ($this->deleted_at == '');
}
@@ -859,6 +891,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
@@ -932,7 +1007,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
/**
* Get the preferred locale for the user.
*
@@ -971,7 +1045,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
public function scopeUserLocation($query, $location, $search)
{
return $query->where('location_id', '=', $location)
->where('users.first_name', 'LIKE', '%' . $search . '%')
->orWhere('users.email', 'LIKE', '%' . $search . '%')
@@ -984,9 +1057,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
->orWhere('users.username', 'LIKE', '%' . $search . '%')
->orwhereRaw('CONCAT(users.first_name," ",users.last_name) LIKE \''.$search.'%\'');
}
/**
@@ -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))
)
)
);
}
}
@@ -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))
)
)
);
}
}
+9 -11
View File
@@ -5,26 +5,23 @@ namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Password;
use App\Models\User;
class WelcomeNotification extends Notification
{
use Queueable;
private $_data = [];
public $expire_date;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(array $content)
public function __construct(public User $user)
{
$this->_data['email'] = htmlspecialchars_decode($content['email']);
$this->_data['first_name'] = htmlspecialchars_decode($content['first_name']);
$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');
$this->user->token = Password::broker('invites')->createToken($user);
$this->user->expire_date = now()->addMinutes((int) config('auth.passwords.invites.expire', 2880))->format('F j, Y, g:i a');
}
/**
@@ -44,8 +41,9 @@ class WelcomeNotification extends Notification
*/
public function toMail()
{
return (new MailMessage())
->subject(trans('mail.welcome', ['name' => $this->_data['first_name'].' '.$this->_data['last_name']]))
->markdown('notifications.Welcome', $this->_data);
->subject(trans('mail.welcome', ['name' => $this->user->first_name.' '.$this->user->last_name]))
->markdown('notifications.Welcome', $this->user->toArray());
}
}
+1
View File
@@ -115,6 +115,7 @@ class AssetPresenter extends Presenter
'sortable' => true,
'title' => trans('admin/users/table.title'),
'visible' => false,
'formatter' => 'jobtitleFormatter',
], [
'field' => 'location',
'searchable' => true,
+2 -2
View File
@@ -106,7 +106,7 @@ class HistoryPresenter extends Presenter
'switchable' => true,
'title' => trans('general.file_name'),
'visible' => true,
'formatter' => 'fileUploadNameFormatter',
'formatter' => 'fileNameFormatter',
],
[
'field' => 'file_download',
@@ -115,7 +115,7 @@ class HistoryPresenter extends Presenter
'switchable' => true,
'title' => trans('general.download'),
'visible' => true,
'formatter' => 'fileUploadFormatter',
'formatter' => 'fileDownloadButtonsFormatter',
],
[
'field' => 'note',
+13 -10
View File
@@ -5,7 +5,7 @@ namespace App\Presenters;
/**
* Class AccessoryPresenter
*/
class UploadsPresenter extends Presenter
class UploadedFilesPresenter extends Presenter
{
/**
* Json Column Layout for bootstrap table
@@ -18,7 +18,7 @@ class UploadsPresenter extends Presenter
$layout = [
[
'field' => 'id',
'searchable' => false,
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('general.id'),
@@ -30,6 +30,7 @@ class UploadsPresenter extends Presenter
'sortable' => false,
'switchable' => false,
'title' => trans('general.type'),
'visible' => true,
'formatter' => 'iconFormatter',
],
[
@@ -38,16 +39,17 @@ class UploadsPresenter extends Presenter
'sortable' => false,
'switchable' => true,
'title' => trans('general.image'),
'formatter' => 'inlineImageFormatter',
'visible' => true,
'formatter' => 'filePreviewFormatter',
],
[
'field' => 'filename',
'searchable' => false,
'sortable' => false,
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('general.file_name'),
'visible' => true,
'formatter' => 'fileUploadNameFormatter',
'formatter' => 'fileNameFormatter',
],
[
'field' => 'download',
@@ -56,7 +58,7 @@ class UploadsPresenter extends Presenter
'switchable' => true,
'title' => trans('general.download'),
'visible' => true,
'formatter' => 'downloadOrOpenInNewWindowFormatter',
'formatter' => 'fileDownloadButtonsFormatter',
],
[
'field' => 'note',
@@ -68,10 +70,10 @@ class UploadsPresenter extends Presenter
],
[
'field' => 'created_by',
'searchable' => false,
'searchable' => true,
'sortable' => true,
'title' => trans('general.created_by'),
'visible' => false,
'visible' => true,
'formatter' => 'usersLinkObjFormatter',
],
[
@@ -80,7 +82,7 @@ class UploadsPresenter extends Presenter
'sortable' => true,
'switchable' => true,
'title' => trans('general.created_at'),
'visible' => false,
'visible' => true,
'formatter' => 'dateDisplayFormatter',
], [
'field' => 'available_actions',
@@ -88,6 +90,7 @@ class UploadsPresenter extends Presenter
'sortable' => false,
'switchable' => false,
'title' => trans('table.actions'),
'visible' => true,
'formatter' => 'deleteUploadFormatter',
],
];
+1 -1
View File
@@ -180,7 +180,7 @@ class UserPresenter extends Presenter
'switchable' => false,
'title' => trans('admin/users/table.username'),
'visible' => true,
'formatter' => 'usersLinkFormatter',
'formatter' => 'usernameRoleLinkFormatter',
],
[
'field' => 'employee_num',
+49 -1
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.
*
*/
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()) {
return true;
}
});
/**
* 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.
*/
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) {
if ($user->hasAccess('admin')) {
return true;
@@ -249,5 +296,6 @@ class AuthServiceProvider extends ServiceProvider
return $user->canEditProfile();
});
}
}
@@ -539,6 +539,16 @@ class BreadcrumbsServiceProvider extends ServiceProvider
->push(trans('general.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'))
);
} else {
Breadcrumbs::for('users.index', fn(Trail $trail) => $trail->parent('home', route('home'))
->push(trans('general.users'), route('users.index'))
+3 -1
View File
@@ -5,9 +5,11 @@ namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Validation\Concerns\ValidatesAttributes;
class AlphaEncrypted implements ValidationRule
{
use ValidatesAttributes;
/**
* Run the validation rule.
*
@@ -18,7 +20,7 @@ class AlphaEncrypted implements ValidationRule
try {
$attributeName = trim(preg_replace('/_+|snipeit|\d+/', ' ', $attribute));
$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]));
}
} catch (\Exception $e) {
+33
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'));
}
}
}
+32
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'));
}
}
}
+32
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'));
}
}
}
+32
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 IPEncrypted 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->validateIp($attributeName, $decrypted) && !is_null($decrypted)) {
$fail(trans('validation.ip', ['attribute' => $attributeName]));
}
} catch (\Exception $e) {
report($e);
$fail(trans('general.something_went_wrong'));
}
}
}
+32
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 IPv4Encrypted 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->validateIpv4($attributeName, $decrypted) && !is_null($decrypted)) {
$fail(trans('validation.ipv4', ['attribute' => $attributeName]));
}
} catch (\Exception $e) {
report($e);
$fail(trans('general.something_went_wrong'));
}
}
}
+32
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 IPv6Encrypted 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->validateIpv6($attributeName, $decrypted) && !is_null($decrypted)) {
$fail(trans('validation.ipv6', ['attribute' => $attributeName]));
}
} catch (\Exception $e) {
report($e);
$fail(trans('general.something_went_wrong'));
}
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Rules;
use App\Models\CustomField;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Validation\Concerns\ValidatesAttributes;
class MacEncrypted 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->validateRegex($attributeName, $decrypted, ['/^[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}$/']) && !is_null($decrypted)) {
$fail(trans('validation.mac_address', ['attribute' => $attributeName]));
}
} catch (\Exception $e) {
report($e);
$fail(trans('general.something_went_wrong'));
}
}
}
+5 -2
View File
@@ -3,12 +3,16 @@
namespace App\Rules;
use Closure;
use Egulias\EmailValidator\Validation\RFCValidation;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Validation\Concerns\ValidatesAttributes;
class NumericEncrypted implements ValidationRule
{
use ValidatesAttributes;
/**
* Run the validation rule.
*
@@ -16,11 +20,10 @@ class NumericEncrypted implements ValidationRule
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
$attributeName = trim(preg_replace('/_+|snipeit|\d+/', ' ', $attribute));
$decrypted = Crypt::decrypt($value);
if (!is_numeric($decrypted) && !is_null($decrypted)) {
if (!$this->validateNumeric($attributeName, $decrypted) && !is_null($decrypted)) {
$fail(trans('validation.numeric', ['attribute' => $attributeName]));
}
} catch (\Exception $e) {
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Rules;
use App\Models\CustomField;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Validation\Concerns\ValidatesAttributes;
class RegexEncrypted 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
{
$field = CustomField::where('db_column', $attribute)->first();
$regex = $field->format;
$regex = str_replace('regex:', '', $regex);
try {
$attributeName = trim(preg_replace('/_+|snipeit|\d+/', ' ', $attribute));
$decrypted = Crypt::decrypt($value);
if (!$this->validateRegex($attributeName, $decrypted, [$regex]) && !is_null($decrypted)) {
$fail(trans('validation.regex', ['attribute' => $attributeName]));
}
} catch (\Exception $e) {
report($e->getMessage());
$fail(trans('general.something_went_wrong'));
}
}
}
+32
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 UrlEncrypted 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->validateUrl($attributeName, $decrypted, []) && !is_null($decrypted)) {
$fail(trans('validation.url', ['attribute' => $attributeName]));
}
} catch (\Exception $e) {
report($e);
$fail(trans('general.something_went_wrong'));
}
}
}
+1 -1
View File
@@ -37,7 +37,7 @@
"doctrine/dbal": "^3.1",
"doctrine/instantiator": "^1.3",
"eduardokum/laravel-mail-auto-embed": "^2.0",
"enshrined/svg-sanitize": "^0.15.0",
"enshrined/svg-sanitize": "^0.16.0",
"erusev/parsedown": "^1.7",
"fakerphp/faker": "^1.24",
"guzzlehttp/guzzle": "^7.0.1",
Generated
+1204 -1091
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -104,6 +104,16 @@ return [
]
],
'invites' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => env('INVITE_PASSWORD_LINK_EXPIRES', 2880),
'throttle' => [
'max_attempts' => env('LOGIN_MAX_ATTEMPTS', 5),
'lockout_duration' => env('LOGIN_LOCKOUT_DURATION', 60),
]
],
],
/*
+1 -1
View File
@@ -18,7 +18,7 @@ $dump_options = [
//'add_extra_option' => '--optionname=optionvalue',
];
// Some versions of mysql do not support the --skip-ssl option and will fail if it is
// Some versions of mysql do not support the --skip-ssl option and will fail if it is even set
if (env('DB_DUMP_SKIP_SSL') == 'true') {
$dump_options['skip_ssl'] = true;
}
+30 -162
View File
@@ -4,50 +4,31 @@
|--------------------------------------------------------------------------
| DO NOT EDIT THIS FILE DIRECTLY.
|--------------------------------------------------------------------------
| This file reads from your .env configuration file and should not
| This file reads from your .env configuration file and should NOT
| be modified directly.
| See https://snipe-it.readme.io/docs/configuration for more info.
*/
return [
/*
| ***************************************************************************
| DO NOT MAKE CHANGES DIRECTLY TO THIS FILE.
|
| Instead use your .env file to set your application configuration settings.
| See https://snipe-it.readme.io/docs/configuration for more info.
|
|--------------------------------------------------------------------------
| Mail Driver
|--------------------------------------------------------------------------
|
| Laravel supports both SMTP and PHP's "mail" function as drivers for the
| sending of e-mail. You may specify which one you're using throughout
| your application here. By default, Laravel is setup for SMTP mail.
|
|
| Supported: "smtp", "mail", "sendmail", "mailgun", "mandrill", "ses", "log"
|
*/
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send any email
| messages sent by your application. Alternative mailers may be setup
| and used as needed; however, this mailer will be used by default.
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
| Previous versions of Snipe-IT and Laravel used SwiftMailer, which used
| MAIL_DRIVER instead of MAIL_MAILER. In order to not break existing
| installations, we'll accept both
| installations, we'll accept both.
|
*/
'default' => env('MAIL_DRIVER') != null ? env('MAIL_DRIVER') : env('MAIL_MAILER', 'smtp'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
@@ -57,112 +38,29 @@ return [
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers to be used while
| sending an e-mail. You will specify which one you are using for your
| mailers below. You are free to add additional mailers as required.
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "log", "array", "failover", "roundrobin"
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'smtp' => [
// Don't touch this. For the smtp mailer, the transport needs to be smtp
'transport' => 'smtp',
/*
|--------------------------------------------------------------------------
| SMTP Host Address
|--------------------------------------------------------------------------
|
| Here you may provide the host address of the SMTP server used by your
| applications. A default option is provided that is compatible with
| the Mailgun mail service which will provide reliable deliveries.
|
*/
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
/*
|--------------------------------------------------------------------------
| SMTP Host Port
|--------------------------------------------------------------------------
|
| This is the SMTP port used by your application to deliver e-mails to
| users of the application. Like the host we have set this value to
| stay compatible with the Mailgun e-mail application by default.
|
*/
'port' => env('MAIL_PORT', 587),
/*
|--------------------------------------------------------------------------
| SMTP Server Username
|--------------------------------------------------------------------------
|
| If your SMTP server requires a username for authentication, you should
| set it here. This will get used to authenticate with your server on
| connection. You may also set the "password" value below this one.
|
*/
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
/*
|--------------------------------------------------------------------------
| SMTP Server Password
|--------------------------------------------------------------------------
|
| Here you may set the password required by your SMTP server to send out
| messages from your application. This will be given to the server on
| connection so that the application will be able to send messages.
|
*/
'password' => env('MAIL_PASSWORD'),
/*
|--------------------------------------------------------------------------
| SMTP Server connection timeout
|--------------------------------------------------------------------------
|
| Give up trying to connect to the mail server after this many seconds
|
*/
'timeout' => 30,
/*
|--------------------------------------------------------------------------
| mostly pointless encryption option
|--------------------------------------------------------------------------
|
| This used to let you specify whether or not to use TLS, but now it really
| just helps inform the code about which SMTP port to use. SymfonMailer will
| still use TLS if the server offers it, regardless of how this is set.
| We're only including it here for reference, and the extreme edge-case where
| Symfony can't figure out what to do on its own.
|
*/
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
/*
|--------------------------------------------------------------------------
| Verify SMTP TLS certificate
|--------------------------------------------------------------------------
|
| As noted above, Swiftmailer WILL use TLS if the server offers it, so if
| you're using a self-signed certificate or want to refer to your mailserver
| by a name that isn't in the certificate (like the IP address), set this
| to 0 or false.
|
*/
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
'verify_peer' => env('MAIL_TLS_VERIFY_PEER', 1),
],
'ses' => [
@@ -171,29 +69,16 @@ return [
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => null,
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'mailgun' => [
'transport' => 'mailgun',
// 'client' => [
// 'timeout' => 5,
// ],
'resend' => [
'transport' => 'resend',
],
/*
|--------------------------------------------------------------------------
| Sendmail System Path
|--------------------------------------------------------------------------
|
| When using the "sendmail" driver to send e-mails, we will need to know
| the path to where Sendmail lives on this server. A default path has
| been provided here, which will work well on most of your systems.
|
*/
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
@@ -223,6 +108,7 @@ return [
'postmark',
],
],
],
/*
@@ -230,15 +116,15 @@ return [
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all e-mails sent by your application to be sent from
| the same address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDR', null),
'name' => env('MAIL_FROM_NAME', null),
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
/*
@@ -257,23 +143,5 @@ return [
'name' => env('MAIL_REPLYTO_NAME', null),
],
/*
|--------------------------------------------------------------------------
| Markdown Mail Settings
|--------------------------------------------------------------------------
|
| If you are using Markdown based email rendering, you may configure your
| theme and component paths here, allowing you to customize the design
| of the emails. Or, you may simply stick with the Laravel defaults!
|
*/
'markdown' => [
'theme' => 'default',
'paths' => [
resource_path('views/vendor/mail'),
],
],
];
];
+5 -5
View File
@@ -1,10 +1,10 @@
<?php
return array (
'app_version' => 'v8.1.18',
'full_app_version' => 'v8.1.18 - build 18876-g4c6249eb9',
'build_version' => '18876',
'app_version' => 'v8.2.1',
'full_app_version' => 'v8.2.1 - build 19068-g6ca49a20c',
'build_version' => '19068',
'prerelease_version' => '',
'hash_version' => 'g4c6249eb9',
'full_hash' => 'v8.1.18-77-g4c6249eb9',
'hash_version' => 'g6ca49a20c',
'full_hash' => 'v8.2.1-10-g6ca49a20c',
'branch' => 'develop',
);
+36
View File
@@ -93,6 +93,42 @@ class CustomFieldFactory extends Factory
});
}
public function encrypt()
{
return $this->state(function () {
return [
'field_encrypted' => '1',
];
});
}
public function alpha()
{
return $this->state(function () {
return [
'format' => 'alpha',
];
});
}
public function numeric()
{
return $this->state(function () {
return [
'format' => 'numeric',
];
});
}
public function email()
{
return $this->state(function () {
return [
'format' => 'email',
];
});
}
public function testCheckbox()
{
return $this->state(function () {
@@ -1,8 +1,7 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use \App\Helpers\Helper;
use \App\Models\Setting;
use \App\Models\User;
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
//
Artisan::call("snipeit:clean-checkout-acceptances");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};
-3
View File
@@ -51,8 +51,6 @@ class AssetModelSeeder extends Seeder
$del_files = Storage::files($dst);
foreach ($del_files as $del_file) { // iterate files
$file_to_delete = str_replace($src, '', $del_file);
Log::debug('Deleting: '.$file_to_delete);
try {
Storage::disk('public')->delete($dst.$del_file);
} catch (\Exception $e) {
@@ -63,7 +61,6 @@ class AssetModelSeeder extends Seeder
$add_files = glob($src.'/*.*');
foreach ($add_files as $add_file) {
$file_to_copy = str_replace($src, '', $add_file);
Log::debug('Copying: '.$file_to_copy);
try {
Storage::disk('public')->put($dst.$file_to_copy, file_get_contents($src.$file_to_copy));
} catch (\Exception $e) {
+856 -1151
View File
File diff suppressed because it is too large Load Diff
+8 -9
View File
@@ -17,12 +17,12 @@
},
"devDependencies": {
"all-contributors-cli": "^6.26.1",
"axios": "^1.7.2",
"babel-preset-latest": "^6.24.1",
"axios": "^1.11.0",
"jquery": "<3.6.0",
"laravel-mix": "^6.0.49",
"lodash": "^4.17.20",
"postcss": "^8.4.5"
"postcss": "^8.5.6",
"webpack": "^5.98.0"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
@@ -35,18 +35,18 @@
"bootstrap-colorpicker": "^2.5.3",
"bootstrap-datepicker": "^1.10.0",
"bootstrap-less": "^3.3.8",
"bootstrap-table": "1.24.1",
"bootstrap-table": "1.24.2",
"canvas-confetti": "^1.9.3",
"chart.js": "^2.9.4",
"clipboard": "^2.0.11",
"css-loader": "^5.0.0",
"ekko-lightbox": "^5.1.1",
"imagemin": "^8.0.1",
"imagemin": "^9.0.1",
"jquery-slimscroll": "^1.3.8",
"jquery-ui": "^1.14.1",
"jquery-validation": "^1.21.0",
"jquery.iframe-transport": "^1.0.0",
"jspdf-autotable": "^3.8.4",
"jspdf-autotable": "^5.0.2",
"less": "^4.2.2",
"less-loader": "^6.0",
"list.js": "^1.5.0",
@@ -55,8 +55,7 @@
"select2": "4.0.13",
"sheetjs": "^2.0.0",
"signature_pad": "^4.2.0",
"tableexport.jquery.plugin": "1.32.0",
"tether": "^1.4.0",
"webpack": "^5.98.0"
"tableexport.jquery.plugin": "^1.33.0",
"tether": "^1.4.0"
}
}
+2
View File
@@ -5418,3 +5418,5 @@ hr {
}
}
/*# sourceMappingURL=AdminLTE.css.map*/
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -1266,7 +1266,6 @@ label.form-control {
}
label.form-control--disabled {
color: #959495;
pointer-events: none;
cursor: not-allowed;
}
/** --------------------------------------- **/
@@ -1429,6 +1428,9 @@ input[type="radio"]:checked::before {
.bootstrap-table .fixed-table-container .table tbody tr .card-view {
display: table-row !important;
}
.form-control-static {
padding-top: 0px;
}
td.text-right.text-padding-number-cell {
padding-right: 30px !important;
white-space: nowrap;
@@ -1467,3 +1469,5 @@ caption.tableCaption {
padding-left: 8px;
}
/*# sourceMappingURL=app.css.map*/
File diff suppressed because one or more lines are too long
+5 -1
View File
@@ -887,7 +887,6 @@ label.form-control {
}
label.form-control--disabled {
color: #959495;
pointer-events: none;
cursor: not-allowed;
}
/** --------------------------------------- **/
@@ -1050,6 +1049,9 @@ input[type="radio"]:checked::before {
.bootstrap-table .fixed-table-container .table tbody tr .card-view {
display: table-row !important;
}
.form-control-static {
padding-top: 0px;
}
td.text-right.text-padding-number-cell {
padding-right: 30px !important;
white-space: nowrap;
@@ -1088,3 +1090,5 @@ caption.tableCaption {
padding-left: 8px;
}
/*# sourceMappingURL=overrides.css.map*/
File diff suppressed because one or more lines are too long
+11 -3
View File
@@ -20167,6 +20167,7 @@ hr {
}
/*# sourceMappingURL=AdminLTE.css.map*/
/*!
* Datepicker for Bootstrap v1.10.0 (https://github.com/uxsolutions/bootstrap-datepicker)
*
@@ -21008,7 +21009,7 @@ hr {
@charset "UTF-8";
/**
* @author zhixin wen <wenzhixin2010@gmail.com>
* version: 1.24.1
* version: 1.24.2
* https://github.com/wenzhixin/bootstrap-table/
*/
/* stylelint-disable annotation-no-unknown, max-line-length */
@@ -22601,7 +22602,6 @@ label.form-control {
}
label.form-control--disabled {
color: #959495;
pointer-events: none;
cursor: not-allowed;
}
/** --------------------------------------- **/
@@ -22764,6 +22764,9 @@ input[type="radio"]:checked::before {
.bootstrap-table .fixed-table-container .table tbody tr .card-view {
display: table-row !important;
}
.form-control-static {
padding-top: 0px;
}
td.text-right.text-padding-number-cell {
padding-right: 30px !important;
white-space: nowrap;
@@ -22803,6 +22806,7 @@ caption.tableCaption {
}
/*# sourceMappingURL=app.css.map*/
.select2-container {
box-sizing: border-box;
display: inline-block;
@@ -24174,7 +24178,6 @@ label.form-control {
}
label.form-control--disabled {
color: #959495;
pointer-events: none;
cursor: not-allowed;
}
/** --------------------------------------- **/
@@ -24337,6 +24340,9 @@ input[type="radio"]:checked::before {
.bootstrap-table .fixed-table-container .table tbody tr .card-view {
display: table-row !important;
}
.form-control-static {
padding-top: 0px;
}
td.text-right.text-padding-number-cell {
padding-right: 30px !important;
white-space: nowrap;
@@ -24375,3 +24381,5 @@ caption.tableCaption {
padding-left: 8px;
}
/*# sourceMappingURL=overrides.css.map*/
+1 -1
View File
@@ -1,7 +1,7 @@
@charset "UTF-8";
/**
* @author zhixin wen <wenzhixin2010@gmail.com>
* version: 1.24.1
* version: 1.24.2
* https://github.com/wenzhixin/bootstrap-table/
*/
/* stylelint-disable annotation-no-unknown, max-line-length */
+2
View File
@@ -5471,3 +5471,5 @@ tr th div.th-inner {
background-color: var(--back-sub);
}
/*# sourceMappingURL=_all-skins.css.map*/
File diff suppressed because one or more lines are too long
+2
View File
@@ -5471,3 +5471,5 @@ tr th div.th-inner {
background-color: var(--back-sub);
}
/*# sourceMappingURL=_all-skins.css.map*/
+2
View File
@@ -557,3 +557,5 @@ div.container.row-new-striped {
background-color: var(--back-sub);
}
/*# sourceMappingURL=skin-black-dark.css.map*/
File diff suppressed because one or more lines are too long
+2
View File
@@ -557,3 +557,5 @@ div.container.row-new-striped {
background-color: var(--back-sub);
}
/*# sourceMappingURL=skin-black-dark.css.map*/
+2
View File
@@ -217,3 +217,5 @@ a.btn-danger:visited {
background-color: #e9d15b;
}
/*# sourceMappingURL=skin-black.css.map*/
File diff suppressed because one or more lines are too long
+2
View File
@@ -217,3 +217,5 @@ a.btn-danger:visited {
background-color: #e9d15b;
}
/*# sourceMappingURL=skin-black.css.map*/
+2
View File
@@ -537,3 +537,5 @@ a:visited {
background-color: var(--back-sub);
}
/*# sourceMappingURL=skin-blue-dark.css.map*/
File diff suppressed because one or more lines are too long
+2
View File
@@ -537,3 +537,5 @@ a:visited {
background-color: var(--back-sub);
}
/*# sourceMappingURL=skin-blue-dark.css.map*/
+2
View File
@@ -249,3 +249,5 @@ a.label.label-default:hover {
color: #296282;
}
/*# sourceMappingURL=skin-blue.css.map*/
File diff suppressed because one or more lines are too long
+2
View File
@@ -249,3 +249,5 @@ a.label.label-default:hover {
color: #296282;
}
/*# sourceMappingURL=skin-blue.css.map*/
+2
View File
@@ -225,3 +225,5 @@ input::-ms-input-placeholder {
background-color: #e9d15b;
}
/*# sourceMappingURL=skin-contrast.css.map*/
File diff suppressed because one or more lines are too long
+2
View File
@@ -225,3 +225,5 @@ input::-ms-input-placeholder {
background-color: #e9d15b;
}
/*# sourceMappingURL=skin-contrast.css.map*/
+2
View File
@@ -524,3 +524,5 @@ a:visited {
background-color: var(--back-sub);
}
/*# sourceMappingURL=skin-green-dark.css.map*/
File diff suppressed because one or more lines are too long
+2
View File
@@ -524,3 +524,5 @@ a:visited {
background-color: var(--back-sub);
}
/*# sourceMappingURL=skin-green-dark.css.map*/
+2
View File
@@ -209,3 +209,5 @@ a:visited {
background-color: #e9d15b;
}
/*# sourceMappingURL=skin-green.css.map*/
File diff suppressed because one or more lines are too long
+2
View File
@@ -209,3 +209,5 @@ a:visited {
background-color: #e9d15b;
}
/*# sourceMappingURL=skin-green.css.map*/

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