Compare commits

..

525 Commits

Author SHA1 Message Date
snipe d535adcc5e Merge branch 'refs/heads/develop' 2013-11-29 17:09:07 -05:00
snipe f62ce4039d @uberbrady is a pedantic shitbag. 2013-11-29 17:07:49 -05:00
snipe 8f9a82e400 Prevent user editing on deleted user 2013-11-29 17:05:55 -05:00
snipe 2be6c6f5da Hurr. 2013-11-29 16:57:49 -05:00
snipe 35155dfc4c Prevent user from being deleted if they still have assets assigned to them 2013-11-29 16:57:24 -05:00
snipe 7142e37372 Deleted users 2013-11-29 16:44:48 -05:00
snipe dca7a7f2eb Allow deleted users 2013-11-29 16:39:24 -05:00
snipe a7ca9f0c9f Layout tweaks 2013-11-29 16:36:11 -05:00
snipe bc16276a38 Merge pull request #96 from flashingcursor/patch-1
Removed post-install-cmd php artisan optimize.
2013-11-29 13:10:47 -08:00
snipe 3420f3ed8b Merge branch 'refs/heads/develop' 2013-11-29 16:09:36 -05:00
snipe 11c657ffa4 Fixes #36 2013-11-29 16:08:18 -05:00
snipe 80afa7cbc7 Disallow checkout of undeployable items 2013-11-29 15:27:44 -05:00
snipe 75fc552637 Merge branch 'refs/heads/develop' 2013-11-29 15:22:54 -05:00
snipe 56adc67e67 Require manufacturer ID 2013-11-29 15:21:11 -05:00
snipe 02f1977d30 Fixed error message 2013-11-29 15:20:29 -05:00
snipe b68e39472d Updating to bootstrap 3 2013-11-29 15:19:00 -05:00
snipe 10df2bc950 Bootstrap 3 2013-11-29 14:46:38 -05:00
Gil Rutkowski f6aecd8af9 Removed post-install-cmd php artisan optimize. Causes initial install to fail due to lack of required config (mail.php) 2013-11-29 14:37:44 -05:00
snipe 567f395afa More fixes for #88 2013-11-29 14:37:06 -05:00
snipe 7003eff42c More fixes for #88 2013-11-29 14:36:46 -05:00
snipe d54eb94497 Improved layout 2013-11-29 14:35:24 -05:00
snipe e305099b45 Fixed model view 2013-11-29 09:52:51 -05:00
snipe 091de54bab BS3 error validation text 2013-11-29 09:45:30 -05:00
snipe 112d5d5994 Style change - removed underline from header 2013-11-29 09:35:51 -05:00
snipe 428998c6fb Prettier buttons 2013-11-29 09:32:10 -05:00
snipe 56b52392e8 honor user soft-deletes 2013-11-29 09:28:45 -05:00
snipe ddf0997a93 Signin BS3 2013-11-29 09:05:30 -05:00
snipe e1207e0e27 Merge branch 'refs/heads/develop' 2013-11-29 08:41:47 -05:00
snipe 3544be0960 Merge branch 'refs/heads/feature-bootstrap3' into develop 2013-11-29 08:41:24 -05:00
snipe 2e916d0246 MOAR BOOTSTRAP 2013-11-29 08:39:10 -05:00
snipe 809e176908 Updating for bootstrap3 2013-11-29 07:39:41 -05:00
snipe c2bf033fb8 More form/button class changes 2013-11-29 07:11:33 -05:00
snipe d67c01e652 Whitespace 2013-11-29 05:21:00 -05:00
snipe 8d82338e1e Remove underline from stats blocks 2013-11-29 05:20:49 -05:00
snipe 89e6f52142 More bootstrap 3 2013-11-29 05:15:51 -05:00
snipe 030db6e267 Upgrading to bootstrap 3
Ugh.
2013-11-29 04:10:25 -05:00
snipe aae590adc4 Merge branch 'refs/heads/develop' 2013-11-28 23:33:33 -05:00
snipe 06cbd0723e Fixes #93 2013-11-28 23:32:44 -05:00
snipe ddabdaca02 Merge branch 'refs/heads/develop' 2013-11-28 20:59:05 -05:00
snipe 839d01a3af Fixed relationship method because fuck you ORM 2013-11-28 20:58:40 -05:00
snipe 4d421c9a50 Leftover method that's not used since the license/asset split 2013-11-28 20:58:15 -05:00
snipe 9fa826b2e7 Fixed assigned to id 2013-11-28 20:40:02 -05:00
snipe 7718e8d928 Checks that checkedout_to isn't 0 before trying to get fullName() 2013-11-28 20:28:09 -05:00
snipe 23374b7fc6 Another leftover from #88 2013-11-28 20:27:44 -05:00
snipe f0f07b578b Fixing pivot for users 2013-11-28 20:19:23 -05:00
snipe e23f1f10b7 Switched validation to alpha_space on notes 2013-11-28 20:02:11 -05:00
snipe 1b56c31a22 Fixed relationship between users/locations because fuck you ORM 2013-11-28 19:58:16 -05:00
snipe 9f7cbc8a4f Another fix related to #88, also accounts for users with invalid location_ids 2013-11-28 19:57:42 -05:00
snipe f504175578 Smaller char req for first/last name, longer req for password 2013-11-28 19:45:47 -05:00
snipe 103faca6ef Clearer display of current vs trashed 2013-11-28 19:45:25 -05:00
snipe 2fce4884d7 Cosmetic changes 2013-11-28 19:45:05 -05:00
snipe 4142629665 Layout tweaks for mobile - related to #54 2013-11-28 19:22:39 -05:00
snipe 03821838eb Merge branch 'refs/heads/develop' 2013-11-28 18:48:15 -05:00
snipe d2959e82a1 Redirect to /hardware to finish cleaning up re-routing from #88 2013-11-28 18:47:54 -05:00
snipe 6a5ccc89df Updated urls from #88 2013-11-28 18:44:20 -05:00
snipe 5858bfb99b Fixed language file paths caused by #88 2013-11-28 18:44:04 -05:00
snipe 3cce78c087 Weird layout 2013-11-28 18:43:30 -05:00
snipe 0d0d42f86a Merge branch 'refs/heads/develop' 2013-11-28 18:32:07 -05:00
snipe d9a37f3822 Fixes #88 2013-11-28 18:31:36 -05:00
snipe 4fcdf72ab6 Responsive design bug fixes, placeholder code for #64 (commented out) 2013-11-28 16:50:53 -05:00
snipe 8680d98bc3 Fixed status display bug 2013-11-28 16:46:34 -05:00
snipe b3bb2d4f11 Added soon-out-of-warranty example to test notifications 2013-11-28 16:37:24 -05:00
snipe 5c28089f2b RTD has to be 0, or it will conflict with the custom status labels 2013-11-28 16:37:05 -05:00
snipe c3769adf80 Merge branch 'refs/heads/develop' 2013-11-28 15:52:23 -05:00
snipe 9e189aa309 Fixes #80, also prevents the user from checking out an asset that is undeployable, etc 2013-11-28 15:52:00 -05:00
snipe 7a3d92522e Merge pull request #86 from peelman/develop
Cancel and back buttons on edit forms now take you back properly
2013-11-28 11:13:32 -08:00
Nick Peelman 54013cfd42 Cancel and back buttons on edit forms now take you back properly 2013-11-28 10:23:52 -05:00
snipe ccb7a5ddba Merge branch 'refs/heads/develop' 2013-11-28 01:16:05 -05:00
snipe 54d9d72549 Tweaks to example configs to prevent data leakage 2013-11-28 01:15:30 -05:00
snipe 819a606204 Changed modelno validator to alpha_space 2013-11-28 00:58:34 -05:00
snipe 33f81abcd3 Added a few more symbols for alpha_space 2013-11-28 00:58:08 -05:00
snipe fbb0b22522 vi or GTFO
:P
2013-11-28 00:57:40 -05:00
snipe 6b017d12c0 Merge pull request #85 from snipe/gitignore_clean
Replace enumeration of files with wildcard
2013-11-27 21:33:23 -08:00
Brady Wetherington 6691447624 Replace enumeration of files with wildcard 2013-11-27 19:26:53 -05:00
snipe 39cc0c6ad7 Merge pull request #83 from peelman/IgorningProductionConfigs
Igorning production configs
2013-11-27 16:22:10 -08:00
Nick Peelman cb87b16e28 its code, not a block quote...
i are stupid.
2013-11-27 19:22:04 -05:00
Nick Peelman 5be13d5eb5 Ignore production configs and sqlite files. Provide example files for renaming. Update install instructions in README accordingly. 2013-11-27 19:19:07 -05:00
snipe eb015b1e22 Merge branch 'refs/heads/develop' 2013-11-27 14:14:41 -05:00
snipe 012a889444 Corrected license seat count associated with license. Fixes #75 2013-11-27 14:13:29 -05:00
snipe e79d27c984 Merge branch 'refs/heads/develop' 2013-11-27 12:26:15 -05:00
snipe 6552401fd8 Fixes #77 2013-11-27 12:25:48 -05:00
snipe da96126445 Merge pull request #78 from peelman/ExpandManufacturer
Expand Manufacturer
2013-11-27 09:05:10 -08:00
Nick Peelman 301ef3fddb Add Manufacturer and Model info to asset page 2013-11-27 11:37:22 -05:00
Nick Peelman d6ba98252d Change N/A to Select One 2013-11-27 11:37:02 -05:00
snipe 9aa6b519c9 Wider display for address field in index blade 2013-11-27 10:34:51 -05:00
Nick Peelman 9f19792985 Save manufacturer ID 2013-11-27 10:32:50 -05:00
snipe e4d56dba92 Merge pull request #73 from peelman/EmailInputTypes
Email input types
2013-11-27 07:28:15 -08:00
snipe e49cb9eb85 Merge pull request #72 from peelman/AddAddressToLocation
Add address to location form
2013-11-27 07:27:45 -08:00
Nick Peelman b7579a1420 Copy/Paste Error 2013-11-27 10:25:25 -05:00
Nick Peelman 6f59617546 Add to controller's edit method too... 2013-11-27 10:23:11 -05:00
Nick Peelman 996458dde3 missing semicolon 2013-11-27 10:18:43 -05:00
Nick Peelman c2d889de12 Add necessary use statements, clean up the run-on return statement that was forming... 2013-11-27 10:18:15 -05:00
Nick Peelman b5a0da03a8 Add manufacturer to edit form 2013-11-27 10:06:36 -05:00
Nick Peelman 8d3d337820 Change input type to email where relevant (better mobile support) 2013-11-27 08:42:31 -05:00
Nick Peelman 1314a1ffea Add location address values to controller save methods... 2013-11-27 08:30:51 -05:00
Nick Peelman bed78dd935 Remove erroneous rule from address2 2013-11-27 08:28:00 -05:00
Nick Peelman 02e5333101 Add address fields to Location edit form 2013-11-27 08:21:12 -05:00
snipe 0aa3c676c2 Merge branch 'refs/heads/develop' 2013-11-27 03:40:21 -05:00
snipe acd9505597 Two-digit decimal for purchase_cost 2013-11-27 03:40:01 -05:00
snipe 65cd28aad3 Fixes #14 2013-11-27 03:39:28 -05:00
snipe 12684b214a Merge branch 'refs/heads/develop' 2013-11-27 02:57:21 -05:00
snipe 5c72aabdea Updated readme for DB migrations reminder 2013-11-27 02:57:02 -05:00
snipe a2b353fb6d Merge branch 'refs/heads/develop' 2013-11-27 02:41:04 -05:00
snipe aa38d83ba6 Fixes #63 and #70 2013-11-27 02:38:16 -05:00
snipe af69467bea Added notes to log table 2013-11-27 01:29:57 -05:00
snipe 432600ab7b Merge branch 'refs/heads/develop' 2013-11-27 01:21:19 -05:00
snipe b1aa67ddb7 Fixes #69
Removed problem migration file
2013-11-27 01:20:57 -05:00
snipe c16382e9d4 Merge branch 'refs/heads/develop' 2013-11-26 22:07:26 -05:00
snipe 9f9bb96076 Merge pull request #67 from abrahamvegh/patch-1
Patch 1
2013-11-26 18:56:54 -08:00
snipe f1e280ccde Merge pull request #68 from abrahamvegh/patch-2
Fix insecure content warnings
2013-11-26 18:55:14 -08:00
Abraham Vegh cfe28ad903 Update default.blade.php
Remove protocol from Google and jQuery CDN URLs, to fix insecure content warnings in SSL-only environments
2013-11-26 20:33:44 -05:00
Abraham Vegh bda827c590 Update User.php
Fix incorrect Gravatar domain name
2013-11-26 20:19:30 -05:00
Abraham Vegh ebc40857e4 Update AssetsController.php
Fix invalid SQL
2013-11-26 20:15:41 -05:00
snipe 69ede7f82c Merge branch 'refs/heads/develop' 2013-11-26 18:11:00 -05:00
snipe 0e3bcfecd0 Should be final fixes for mysql strict mode 2013-11-26 18:10:46 -05:00
snipe 96368444ac Default user id 2013-11-26 17:53:03 -05:00
snipe ec4172d41a Merge branch 'refs/heads/develop' 2013-11-26 17:49:51 -05:00
snipe 8eeae70313 Formatting cleanup 2013-11-26 17:48:51 -05:00
snipe a33435ee12 No seriously. Fuck you, MySQL strict. 2013-11-26 17:47:21 -05:00
snipe eb7801abf9 Fuck you, MySQL strict mode. Right in the eye. 2013-11-26 17:36:06 -05:00
snipe 42450d9cd6 Merge branch 'refs/heads/master' into develop 2013-11-26 17:24:05 -05:00
snipe 4c0af1eae1 Fixed typo causing failure with mysql strict 2013-11-26 17:22:40 -05:00
snipe 35e5df8bdc Merge branch 'refs/heads/develop' 2013-11-26 16:51:44 -05:00
snipe dfd374b875 Removed optional from db seed 2013-11-26 16:51:23 -05:00
snipe 7722c50312 Merge branch 'refs/heads/develop' 2013-11-26 16:37:09 -05:00
snipe 1527b88fd6 Fixes #61 2013-11-26 16:36:42 -05:00
snipe 7f82a6e9fa Merge branch 'refs/heads/develop' 2013-11-26 14:55:49 -05:00
snipe 196710dac8 Fixed bug in error validation display 2013-11-26 14:55:04 -05:00
snipe 194c0b6c7b Implemented new alpha_space validation 2013-11-26 14:54:46 -05:00
snipe b3b67abaa0 New alpha_space custom validation 2013-11-26 14:08:17 -05:00
snipe 0f0059342c CSRF routing
Not sure if this method will work
2013-11-26 12:49:04 -05:00
snipe d7a8cac1dc Fixed validation 2013-11-26 12:46:23 -05:00
snipe c8288b430e Merge branch 'refs/heads/develop' 2013-11-26 11:57:39 -05:00
snipe dc5ad87686 Fixes #59 2013-11-26 11:57:14 -05:00
snipe 1ff93f65b3 Merge branch 'refs/heads/develop' 2013-11-26 07:29:08 -05:00
snipe 4ccb93665b Sexy typeahead select2 boxes for license and asset checkout 2013-11-26 07:28:47 -05:00
snipe 32a63cdd56 Switched asset->id to license->id 2013-11-26 07:26:07 -05:00
snipe 635cd71ef7 Herp-derp. Braces. 2013-11-26 07:23:47 -05:00
snipe 116af7e809 Merge branch 'refs/heads/develop' 2013-11-26 07:12:14 -05:00
snipe ee3fb2785e Fixed XSS on modal
Weird thing. The values were being escaped on the way out but something in the modal javascript was parsing them out as if they weren't. Using data-html="false" seemed to have no effect on the modal. *shrug* Problem for another time.
2013-11-26 07:05:04 -05:00
snipe 2b1ddd9a45 Escaping HTML in modal title 2013-11-26 06:58:14 -05:00
snipe 1fe925ad5f Removing alpha_dash 2013-11-26 06:56:54 -05:00
snipe c220c0a427 Alpha_dash is too contrictive 2013-11-26 06:56:20 -05:00
snipe 52fa847769 Updated to prevent the XSS in the menu 2013-11-26 06:20:46 -05:00
snipe 9854752036 Don't need it 2013-11-26 04:45:11 -05:00
snipe 37fd854003 Merge branch 'refs/heads/develop' 2013-11-26 03:13:33 -05:00
snipe e6fc1956fc Fixes #55 2013-11-26 03:13:09 -05:00
snipe 5bf38b1d02 Fixes #50 2013-11-26 02:33:16 -05:00
snipe 5b8adda5da Merge branch 'refs/heads/develop' 2013-11-26 02:14:31 -05:00
snipe d0cbb9f3a7 Fixes #52 2013-11-26 02:14:06 -05:00
snipe 1295855302 Fixes #51 2013-11-26 02:12:32 -05:00
snipe 4d1dd1c446 Fixed #50 - MOAR VALIDATION! 2013-11-26 02:09:37 -05:00
snipe e77b47717e More validation 2013-11-26 02:04:29 -05:00
snipe 3f4ce4025f Tighter data validation on name 2013-11-26 01:58:57 -05:00
snipe 680b377bd1 Validation on price 2013-11-26 01:28:45 -05:00
snipe 45c04b1cde Fixed display bug 2013-11-26 01:27:27 -05:00
snipe e13578d97d Fixed error when no category is given 2013-11-26 01:21:55 -05:00
snipe 1d50724c91 Removed parent from category
Since the field was already removed
2013-11-26 01:15:25 -05:00
snipe d469a9601b Merge branch 'refs/heads/develop' 2013-11-25 19:57:40 -05:00
snipe fa246aade1 Removed line thingie 2013-11-25 19:57:28 -05:00
snipe a27be4e053 Merge branch 'refs/heads/develop' 2013-11-25 19:56:41 -05:00
snipe 7e6013b125 Added seat numbers 2013-11-25 19:56:22 -05:00
snipe e310d16fb7 Merge branch 'refs/heads/develop' 2013-11-25 18:33:53 -05:00
snipe 6b224ea8aa Removed dummy text 2013-11-25 18:33:27 -05:00
snipe 7a5b133c58 Fixed bug linking to assets when they should be pointing to licenses 2013-11-25 18:31:59 -05:00
snipe 4e25a9e815 Removed extra columns 2013-11-25 18:31:44 -05:00
snipe 084ec064cb Removed created_at from listing. Not important enough to waste the screen space on 2013-11-25 13:38:14 -05:00
snipe 0eb86ccd1d Merge branch 'refs/heads/develop' 2013-11-25 13:35:32 -05:00
snipe e87e5b9cae Fixes #46 2013-11-25 13:34:52 -05:00
snipe 20cce33273 Merge branch 'refs/heads/develop' 2013-11-25 13:18:49 -05:00
snipe e235a4565c Added manufacturer to model display 2013-11-25 13:16:59 -05:00
snipe a345cbd907 Added view for model in routes 2013-11-25 12:40:09 -05:00
snipe 5959e1f9fc Layout tweaks 2013-11-25 12:39:59 -05:00
snipe 3ea0ad87c8 Fixes #22 2013-11-25 12:39:47 -05:00
snipe cf438d0207 Whitespace 2013-11-25 12:37:26 -05:00
snipe 1624dbffa4 Added model view 2013-11-25 12:37:11 -05:00
snipe e2a1cda50e Changed method name 2013-11-25 12:36:51 -05:00
snipe 6fee1067bc Added depreciate flag for assets and licenses 2013-11-25 11:31:02 -05:00
snipe 3ba2880a35 Layout tweaks 2013-11-25 10:12:41 -05:00
snipe 5a2c6391bf Added category relationship 2013-11-25 10:12:23 -05:00
snipe 447a74928b Ditched parent ID - extraneous feature, not really needed 2013-11-25 10:11:40 -05:00
snipe 62e03e0910 Merge branch 'refs/heads/develop' 2013-11-25 08:26:05 -05:00
snipe 1a970716cc Experimenting with nicer error messages 2013-11-25 08:25:47 -05:00
snipe 57d0f36435 Fixes #4 2013-11-25 08:25:34 -05:00
snipe a8d01550dc Prep for unit testing 2013-11-25 08:09:14 -05:00
snipe f071fe5867 Merge branch 'refs/heads/develop' 2013-11-25 07:57:59 -05:00
snipe acfda8dc82 One more change to tighten the sorting.
Still looks weird, still need to revisit.
2013-11-25 07:57:43 -05:00
snipe 3cc0ed8743 Fixed sorting weirdness
Sort of. Still need to revisit, but this works for now.
2013-11-25 07:55:18 -05:00
snipe ffcfc853b3 Merge branch 'refs/heads/develop' 2013-11-25 07:47:18 -05:00
snipe 051ef0a218 Weird sorting on License table
Need to come back to this
2013-11-25 07:38:06 -05:00
snipe 5fcbc9ba78 ALL THE DATATABLES! 2013-11-25 07:24:51 -05:00
snipe 9dbb147ada Removed pagination, since that's handled via data tables now 2013-11-25 07:07:15 -05:00
snipe 718236b2d4 Fixed #9 2013-11-25 07:06:49 -05:00
snipe efb5269a0c Removed unnecessary image 2013-11-25 07:03:42 -05:00
snipe 74034ae69e Sortable, searchable datatables 2013-11-25 07:03:32 -05:00
snipe 9284164e96 Removed unnecessary graphics 2013-11-25 07:03:15 -05:00
snipe dc26a901d4 More paring down the licenses interface
Still looks assy, but it's a little better now
2013-11-25 06:07:52 -05:00
snipe ad680a337e Fixed column name spelling in DB 2013-11-25 05:47:17 -05:00
snipe dab7ec3457 Sigh. 2013-11-25 05:41:57 -05:00
snipe 46714172ba Spelling is still hard. 2013-11-25 05:41:25 -05:00
snipe 4844cd0b38 Copy edit 2013-11-25 05:40:01 -05:00
snipe 79f93ab475 Merge branch 'refs/heads/develop' 2013-11-25 05:37:02 -05:00
snipe 2b088a3fb2 Fixes #17 2013-11-25 05:36:42 -05:00
snipe 2353eda611 Removed extra columns in license table to make it easier to read, moved buttons for more compact view 2013-11-25 05:12:23 -05:00
snipe 343eb31c56 More info on license page 2013-11-25 05:11:46 -05:00
snipe 56586b7e83 Smaller flat buttons 2013-11-25 05:07:20 -05:00
snipe 1b152396ad Merge branch 'refs/heads/develop' 2013-11-25 04:43:42 -05:00
snipe 69badc7fda Filter action log for hardware 2013-11-25 04:41:03 -05:00
snipe 2727ec13c6 Action log seeder 2013-11-25 04:40:51 -05:00
snipe e457aaee6a Remove software models from model seeder 2013-11-25 04:33:14 -05:00
snipe 03edbc008c Fixes #43 2013-11-25 04:28:51 -05:00
snipe 2203789f4c Fixes #41 2013-11-25 04:00:04 -05:00
snipe 74607c0534 Added set count 2013-11-25 03:59:28 -05:00
snipe 0d9184870c More license tweaks - view blade, asset log 2013-11-25 03:48:54 -05:00
snipe faae6a21d3 More license tweaks 2013-11-25 03:22:38 -05:00
snipe 1b01074b9e Changed license structure to use join table to handle multiple seats 2013-11-25 01:04:39 -05:00
snipe 54894227f0 Removed unneeded 2013-11-25 01:02:27 -05:00
snipe c72e98a193 Drop old tables 2013-11-24 19:07:08 -05:00
snipe 295c2e35eb Edits to status dropdown 2013-11-24 19:06:52 -05:00
snipe 75c48c351c Merge branch 'refs/heads/develop' 2013-11-22 18:16:42 -05:00
snipe 37e1ae7f5a Changed settings table structure
Need to refactor this later
2013-11-22 18:04:16 -05:00
snipe e9bd8397d8 Added site settings into controllers for page_num, added site_name to blade 2013-11-22 18:03:57 -05:00
snipe 9b5274fdcd Nicer title display based on filter 2013-11-22 06:47:51 -05:00
snipe d5bbf38915 View status on show all state 2013-11-22 06:07:03 -05:00
snipe c32d45e901 Fixed ID 2013-11-22 06:01:27 -05:00
snipe 2774b07bce Show asset status on undeployable 2013-11-22 05:54:55 -05:00
snipe a356dbd73d Change table display based on filters 2013-11-22 05:43:40 -05:00
snipe 9249786a25 Fixed status display 2013-11-22 05:37:36 -05:00
snipe 135d21e27d Filtering options on assets page 2013-11-22 05:14:21 -05:00
snipe 95fb2f6efd Include filtering 2013-11-22 04:57:31 -05:00
snipe 6434db2a8e Buttons for asset filtering 2013-11-22 04:31:57 -05:00
snipe d0d94bac44 Merge branch 'refs/heads/develop' 2013-11-22 03:25:25 -05:00
snipe b7251d9992 Fixed potential bug in blade 2013-11-22 03:24:48 -05:00
snipe 1b1efec858 Fixed foreign key in assets 2013-11-22 03:24:34 -05:00
snipe 52020d09f0 Fixed bomb when asset is unassigned 2013-11-22 03:18:11 -05:00
snipe 8dd5704697 Added settings 2013-11-22 03:17:55 -05:00
snipe 7b2cf64d7f DB seed info 2013-11-22 03:17:32 -05:00
snipe 60b577166a Added label for setting readability 2013-11-22 03:17:19 -05:00
snipe da93fb18b8 Added settings controller 2013-11-22 03:16:58 -05:00
snipe a3db6ba99c Comment typo 2013-11-22 03:16:44 -05:00
snipe 08db7fbb38 Hurr. Already had a settings table. 2013-11-22 01:07:41 -05:00
snipe 7b9eebe51c Settings table 2013-11-22 00:53:50 -05:00
snipe 8a348ead33 Merge pull request #38 from rashivkp/develop
Develop
2013-11-21 12:30:56 -08:00
Mohamed Rashid fc8c3c086c fixed #37 2013-11-21 20:34:54 +05:30
snipe 5fae91d4e9 Added status label functionality
remember to run artisan DB migrations to make this work
2013-11-20 22:06:41 -05:00
snipe f7cdc8d5c0 Status addition, seeder stuff 2013-11-20 16:57:45 -05:00
snipe de98f20b9c Added status label table 2013-11-20 08:22:36 -05:00
snipe 904a024636 Status table for non-deployed 2013-11-20 08:10:49 -05:00
snipe 261690986a Prevent orphans in categories 2013-11-20 07:43:18 -05:00
snipe 7ada2cc021 Adding soft-delete to manufacturers 2013-11-20 07:33:54 -05:00
snipe 1b5f0e1dd1 Add check to prevent orphaned models 2013-11-20 07:33:41 -05:00
snipe 595f151ff9 Add soft delete to locations - migration 2013-11-20 07:28:34 -05:00
snipe aa34c4a4dd Add checks to prevent orphaned records 2013-11-20 07:28:24 -05:00
snipe 1d04235520 Associated error messages 2013-11-20 07:27:27 -05:00
snipe ed3fd129bf Get the number of models for the depreciation 2013-11-20 07:27:10 -05:00
snipe 53e6d22f83 Soft delete for locations 2013-11-20 07:26:39 -05:00
snipe f3114dec7a Prevent licenses, models and assets from being deleted if they are assigned to something else 2013-11-20 06:48:25 -05:00
snipe d46556a83d Fixed phone display if no value set 2013-11-20 05:56:22 -05:00
snipe 7f77f1e0b1 Better handling for displaying assets that have been soft-deleted so you don't lose their history 2013-11-20 05:55:18 -05:00
snipe 11a6ee5e8d Merge branch 'refs/heads/develop' 2013-11-20 05:08:41 -05:00
snipe 549c0a9fd6 Linked header boxes
Need to add filters on these asset screens to filter avail vs checked out
2013-11-20 04:54:45 -05:00
snipe 84dbb64caf Removed placeholder copy 2013-11-20 04:32:39 -05:00
snipe c93469c465 Fixed query for asset/license header boxes 2013-11-20 04:31:40 -05:00
snipe 781deb2551 Added live counts to the top callout boxes 2013-11-20 04:12:04 -05:00
snipe 44c5817992 Deleted unnecessary crap 2013-11-20 04:11:42 -05:00
snipe c25122273d Merge branch 'refs/heads/develop' 2013-11-20 01:12:37 -05:00
snipe 4e9aa99e42 Merge branch 'develop' of github.com:snipe/snipe-it into develop
Conflicts:
	app/views/backend/layouts/default.blade.php
2013-11-20 01:08:50 -05:00
snipe fb9f0d837e More detailed modal confirm 2013-11-20 01:05:55 -05:00
snipe 080f38a899 enable soft-deletes on assets 2013-11-20 01:04:35 -05:00
snipe b00f3430cb Added a check where the system wouldn't blow up if asset isn't found 2013-11-20 00:57:21 -05:00
snipe 97a69625ba Please submit your pull requests to develop - thanks! 2013-11-20 00:57:03 -05:00
snipe 34bbf460ee Added modal confirm-delete 2013-11-20 00:56:39 -05:00
snipe 171346b5ea Merge branch 'refs/heads/Chandu-feature-confirm-deletes' into develop 2013-11-19 22:29:11 -05:00
snipe 61cceb5f0b Fixed open tag in CSS 2013-11-19 22:28:35 -05:00
snipe f7032f0eaa Merge branch 'feature-confirm-deletes' of github.com:Chandu/snipe-it into Chandu-feature-confirm-deletes 2013-11-19 22:19:40 -05:00
Chandu e3160957e6 Added confirmation popups for deletes 2013-11-19 21:37:01 -05:00
snipe 57f5f5705f Merge branch 'refs/heads/develop' 2013-11-19 18:28:46 -05:00
snipe 4e8c3517a7 Tightened up display if admin is deleted 2013-11-19 17:50:20 -05:00
snipe 0ee08e47e9 Fixed fatal error if admin who created the asset was deleted. (This should never happen, but...) 2013-11-19 17:48:53 -05:00
snipe 9c08d6fcf7 weird bug 2013-11-19 17:43:55 -05:00
snipe d6a718e997 Fixed bug in views for created_at date 2013-11-19 17:40:06 -05:00
snipe 8daffa82fe Added logging to license checkin/checkout 2013-11-19 17:32:50 -05:00
snipe 7352eaec2e Helper methods for license view display 2013-11-19 17:32:33 -05:00
snipe 67aaeaf7f8 Added route for license view 2013-11-19 17:32:18 -05:00
snipe 76da83cbe1 Fixed links to edit/checkout 2013-11-19 17:31:52 -05:00
snipe 4929f803cc Added view links to index 2013-11-19 17:31:39 -05:00
snipe 24972a330a Added license view blade 2013-11-19 17:31:23 -05:00
snipe 1b160d251e Tidied up the user view, added more info 2013-11-19 17:13:35 -05:00
snipe fa175feb45 Added placeholder "out for digs" option 2013-11-19 17:13:15 -05:00
snipe f0f0019d3b Get the user's location 2013-11-19 17:12:48 -05:00
snipe 8cc367f9e6 Get the admin user details 2013-11-19 17:12:30 -05:00
snipe 55aa41dc32 Tightened up display a little 2013-11-19 16:14:34 -05:00
snipe 08f6d7e17d Added user detail on who it's checked out to 2013-11-19 15:54:59 -05:00
snipe 961baffa4f Improved assetloc function 2013-11-19 15:54:36 -05:00
snipe 9e235b6825 Check asset exists before displaying view 2013-11-19 05:55:58 -05:00
snipe db80b36d0f Added some error checking for invalid users 2013-11-19 05:51:40 -05:00
snipe 02829cb138 Re-ordered columns 2013-11-19 05:51:25 -05:00
snipe 123b6f2a37 Linked assets to user view 2013-11-19 05:31:33 -05:00
snipe 1567a8bde1 Asset view blade 2013-11-19 05:31:07 -05:00
snipe 30a8a96113 Linked asset tags to their new view page 2013-11-19 05:30:54 -05:00
snipe cab75ed7d6 Added asset view to routes 2013-11-19 05:30:32 -05:00
snipe e2c1671d21 Spelling continues to be hard. Apparently. 2013-11-19 05:30:20 -05:00
snipe 5b27a34f05 Fixed fieldname in userlog 2013-11-19 05:30:04 -05:00
snipe ea312fc161 Added asset view in controller 2013-11-19 05:29:42 -05:00
snipe b8908bf246 Add checkin/checkout log to user profile view 2013-11-19 03:58:26 -05:00
snipe f7f79681f3 Added logging action on checkin and checkout 2013-11-19 02:00:17 -05:00
snipe e22d381a20 Added asset logger 2013-11-19 02:00:02 -05:00
snipe 9ebe62f044 Added asset logger 2013-11-19 01:59:45 -05:00
snipe 719c36d482 migration to fix a few fields in the log table 2013-11-19 01:57:34 -05:00
snipe 8c1f6ada67 Number format on diff column 2013-11-18 22:08:00 -05:00
snipe 5bcfca18c7 Added download button, right-aligned fields, added diff math 2013-11-18 22:03:05 -05:00
snipe a28775b9b7 Routes updated for reports 2013-11-18 21:51:02 -05:00
snipe bbaed47e90 Added nav option to get to reports 2013-11-18 21:50:20 -05:00
snipe 8bf1640721 Basic report blade - more to come here 2013-11-18 21:48:44 -05:00
snipe 6265f530ed Added reports controller method 2013-11-18 21:48:28 -05:00
snipe 015c09f560 Swapped people and licenses 2013-11-18 21:48:15 -05:00
snipe 0023b04396 Removed columns from assets view for more compact table
Purchase cost and value are not important day to day. That can be figured out in the reports.
2013-11-18 21:48:03 -05:00
snipe 98cf8767f2 Locked new top boxes down to admin 2013-11-18 21:23:34 -05:00
snipe 3bbd98f1c7 Merge branch 'develop' of github.com:snipe/snipe-it into develop 2013-11-18 21:20:16 -05:00
snipe 532c03a73d Placeholder top summary boxes
There aren't wired up yet
2013-11-18 21:19:30 -05:00
snipe 44ba90f9f0 Fixed icon set to white in main nav 2013-11-18 21:11:50 -05:00
snipe 981ff2b6e5 Asset action log table 2013-11-18 21:10:30 -05:00
Brady Wetherington e819c642db Merge branch 'develop' of github.com:snipe/snipe-it into develop 2013-11-18 15:29:49 -05:00
Brady Wetherington 8268edae81 Merge branch 'develop' of github.com:snipe/snipe-it into develop 2013-11-18 15:16:02 -05:00
snipe edbf195034 Changed footer 2013-11-18 14:52:01 -05:00
snipe 1590c1a300 whitespace 2013-11-18 14:40:32 -05:00
snipe 6dd320171f log files 2013-11-18 14:36:38 -05:00
snipe 5c43caf057 More table fixes 2013-11-18 13:30:48 -05:00
snipe ddfd69e510 More table fixes 2013-11-18 13:23:15 -05:00
snipe 0e11b29cf9 Sigh. 2013-11-18 13:15:38 -05:00
snipe 3401fee672 Another table fix 2013-11-18 13:14:26 -05:00
snipe ffdb4b1aa8 Fixed wonky layout issue 2013-11-18 13:10:38 -05:00
snipe f409ade3a5 So wow. Missed tables disappoint. Such code. 2013-11-18 12:51:20 -05:00
Brady Wetherington 064d7da38e Merge branch 'develop' of github.com:snipe/snipe-it into develop 2013-11-18 12:40:22 -05:00
snipe 4854228851 Moved font awesome files 2013-11-18 12:39:26 -05:00
Brady Wetherington 88599fad7f Merge branch 'develop' of github.com:snipe/snipe-it into develop 2013-11-18 12:37:59 -05:00
snipe d0a5afbf02 Tweaking backend display 2013-11-18 12:36:41 -05:00
snipe c2f5b48635 Removed extraneous images 2013-11-18 12:36:18 -05:00
snipe 5172954aef Added wildcard to nav display 2013-11-18 12:33:32 -05:00
Brady Wetherington 14de269474 Merge branch 'develop' of github.com:snipe/snipe-it into develop 2013-11-18 12:25:23 -05:00
snipe ad914b73b0 Ghetto way to grab location by asset
Need to move this out of the view
2013-11-18 11:51:09 -05:00
snipe c679d446fd Nulled extra values for artisan install 2013-11-18 11:50:17 -05:00
snipe 889b66d578 hurrr 2013-11-18 10:53:20 -05:00
snipe b966bb2889 Added location to asset view 2013-11-18 10:45:14 -05:00
snipe abac6489c4 Removed location_id from assets table
Going to handle this via the people it's checked out to instead
2013-11-18 10:33:35 -05:00
snipe 4ef506cde7 New language entry for no results on user filter 2013-11-18 10:28:03 -05:00
snipe c7c072b07c New fields included in table view 2013-11-18 10:27:48 -05:00
snipe 7b59f42fb0 Fixed pagination bug, styled buttons 2013-11-18 10:27:26 -05:00
snipe 50387874bf Added complete info to the locations seeder 2013-11-18 10:26:47 -05:00
snipe 02ed943ffa New fields in form view 2013-11-18 09:55:07 -05:00
snipe 8f665e82ae Style update for action buttons 2013-11-18 09:54:52 -05:00
snipe 897bc63621 Location drop down to user in views 2013-11-18 09:54:37 -05:00
snipe 673f5accf9 Added location drop down to user 2013-11-18 09:54:19 -05:00
snipe 6214c7b706 migration for new user and location fields 2013-11-18 09:54:00 -05:00
snipe 7c37f91dd5 Linked user in asset view to user profile view 2013-11-18 07:58:20 -05:00
snipe ca90300d2f Added user profile view 2013-11-18 07:53:33 -05:00
snipe d2427bdea2 Cosmetic tweaks 2013-11-18 06:54:12 -05:00
snipe 57107dceee Fixed checkin text
Was mistakenly using checkout text on error/confirmation
2013-11-18 04:54:22 -05:00
snipe 48e180c691 Fixed asset bug caused by unique validation in assets model
Pulled validation back out into the controller for asset tag
2013-11-18 04:50:28 -05:00
snipe b554a9f9db Updated blades with new style buttons 2013-11-18 04:49:54 -05:00
snipe e3fa628bac Reskin files 2013-11-18 04:04:06 -05:00
snipe c29b3c93f7 Removed old bootstrap files 2013-11-18 04:03:20 -05:00
snipe 524d3d233d Added count of assets in asset models table 2013-11-18 01:32:22 -05:00
snipe 2fd8b1760c Removed the note that it's not ready for you yet :) 2013-11-18 01:24:54 -05:00
snipe 7bc7ffb728 Fixed some error reporting/required fields around checkout/checkin 2013-11-18 01:16:08 -05:00
snipe 7d38a7db61 Added checkout/checkin to index display 2013-11-18 00:56:14 -05:00
snipe 785b32c8ff Cleaned up user function for assets 2013-11-18 00:55:57 -05:00
snipe 2d9f56c02a Cleaned up user method 2013-11-18 00:55:36 -05:00
snipe 36bef5bfa5 Language update 2013-11-18 00:55:24 -05:00
snipe ec4239693d Checkin/Checkout for licenses 2013-11-18 00:55:05 -05:00
snipe 17cb895633 Checkin/Checkout for licenses 2013-11-18 00:54:56 -05:00
Brady Wetherington a8372dc998 Merge branch 'develop' of github.com:snipe/snipe-it into develop 2013-11-17 23:44:26 -05:00
snipe d9f434ca12 Short tags FTL 2013-11-17 23:33:21 -05:00
snipe 3a5300e805 license indentation 2013-11-17 23:03:29 -05:00
snipe b22621ab55 typo 2013-11-17 23:03:13 -05:00
snipe 4da2bccbf7 Merge pull request #27 from snipe/depreciation_fix
Depreciation fix
2013-11-17 20:01:46 -08:00
snipe eae1c29262 moar notes!! 2013-11-17 23:00:50 -05:00
Brady Wetherington 127c378b36 Merge branch 'develop' of github.com:snipe/snipe-it into develop 2013-11-17 22:50:22 -05:00
snipe 284645fa7c Added depreciation seed to DatabaseSeeder file
depreciation seed info wasn't being included in the initial seed
2013-11-17 22:49:12 -05:00
snipe 03959b4783 Added note on permissions and documentroot 2013-11-17 22:46:24 -05:00
Brady Wetherington 90c4dfecdf Merge branch 'develop' of github.com:snipe/snipe-it into develop 2013-11-17 22:44:47 -05:00
Brady Wetherington 8f86ec9dae Change depreciation to be by month; formatting fixes. 2013-11-17 22:44:32 -05:00
snipe 3d5dcf7c0e Moved validation to models 2013-11-17 22:34:51 -05:00
snipe 31d323fb71 Cosmetic fix on form 2013-11-17 22:34:43 -05:00
snipe 940281dd4d Fixed bug on validation display 2013-11-17 22:28:59 -05:00
snipe 238e933f00 Moved asset model validation into model 2013-11-17 22:28:49 -05:00
snipe 03e95743e2 Moved manufacturer validation into model 2013-11-17 22:24:01 -05:00
snipe 2f67ee659f Moved validation into models 2013-11-17 22:19:09 -05:00
snipe ea571535b0 extra whitespace 2013-11-17 22:14:32 -05:00
snipe 7bdd5505fd Moved validation to model 2013-11-17 22:14:02 -05:00
snipe 3aea00841a Category using Elegant validation now 2013-11-17 22:05:56 -05:00
snipe 76ac391153 Fixed edit bug where new Category was invoked twice 2013-11-17 22:05:39 -05:00
snipe 7d1a2b2bec Removed validation rules, as they're now in the model 2013-11-17 21:47:59 -05:00
snipe ca842ee447 Added Elegant model and validation rules 2013-11-17 21:47:46 -05:00
snipe 16afe9eb02 Validation abstraction model 2013-11-17 21:46:19 -05:00
snipe 343e13efee Checkout/checkin functionality for assets
Need to refactor a bit and add to licenses as well
2013-11-17 20:36:54 -05:00
snipe a7281ef5b5 programming is hard.
added missing comma
2013-11-17 20:07:42 -05:00
snipe cfb37dcec0 Replaced english text with language reference on buttons 2013-11-17 20:05:12 -05:00
snipe cb5b365631 view logic to determine whether to print out a checkout button 2013-11-17 20:04:43 -05:00
snipe 23c35522c8 Unified save/cancel language option for buttons 2013-11-17 20:04:25 -05:00
snipe 92eab82328 english language messages for checkout 2013-11-17 20:04:13 -05:00
snipe 774b182e9f mark assets as physical, new methods for checkout 2013-11-17 20:03:56 -05:00
snipe 51c1bf4b63 new checkout view 2013-11-17 20:03:34 -05:00
snipe c9a78b319a Added asset checkout routes 2013-11-17 20:03:18 -05:00
snipe f62fdd9aeb Email validation if the license_email field is filled in 2013-11-17 18:41:16 -05:00
snipe 89ede0f8cf Added date picker 2013-11-17 01:53:21 -05:00
snipe 939648e9de Switched to use assets table 2013-11-17 01:53:04 -05:00
snipe 66c9b81118 Seeder changes for new schema 2013-11-17 01:52:44 -05:00
snipe 4143fa6a25 Changes to migrations tables to merge license table 2013-11-17 01:52:18 -05:00
snipe 961a4068b4 Deleted these files - not needed 2013-11-17 01:51:35 -05:00
snipe 38d85afc2b Added where clause for physical = 0 for non-physical assets 2013-11-17 01:51:19 -05:00
snipe 5adb65b4cb Added where clause for physical assets 2013-11-17 01:50:03 -05:00
snipe 17be6b3efe Note about enabling swap in EC2 micros for composer. 2013-11-17 00:14:51 -05:00
snipe a3837eb641 Added note on publishing the debugbar 2013-11-17 00:05:44 -05:00
snipe 90ac04eac7 Edits to DB seeder 2013-11-16 22:42:53 -05:00
snipe eddbb7d973 User/assets tie-in 2013-11-16 22:04:52 -05:00
snipe eee22a9f2b Updated link, added pledgie 2013-11-16 12:47:07 -05:00
snipe 98990d852d Updated Trello url to GH Issues url
Better to keep it all in one place
2013-11-16 12:39:34 -05:00
snipe 33a0d2e191 DB seeds 2013-11-16 11:32:25 -05:00
snipe cf59ec8c67 Fixed pagination bug
Code required
@if ($models->getTotal() > 10)

NOT

@if (count($models) > 10)
2013-11-16 11:32:18 -05:00
snipe 24c3a153e5 Database seeders 2013-11-16 10:57:07 -05:00
snipe ffe786f659 Fixed routes. 2013-11-16 09:33:30 -05:00
snipe 173a7d120f SRSLY WTF OMG.
No idea how that broke.
2013-11-16 09:21:08 -05:00
snipe 4756e950ef oops 2013-11-16 09:17:40 -05:00
snipe 9e96e4b60a very simple lockdown for demo site 2013-11-16 09:03:57 -05:00
snipe c1e1323e68 cleanup 2013-11-16 08:12:25 -05:00
snipe ea7b0c1c0c added alpha reqs to state/country 2013-11-16 07:40:14 -05:00
snipe 27514f85c3 Fixed typo in variable name 2013-11-16 07:19:01 -05:00
snipe 5dc449a99e Added locations CRUD 2013-11-16 07:18:47 -05:00
snipe 7def599141 redirect login to assets page by default, removed dashboard link for now 2013-11-16 06:49:47 -05:00
snipe e4443b8988 Removing unnecessary cruft 2013-11-16 06:38:51 -05:00
snipe bb167aac59 minor cosmetic changes to form 2013-11-16 06:29:14 -05:00
snipe 4742c5e906 require unique asset tags 2013-11-16 06:22:51 -05:00
snipe a62ffa96cf history table, location table 2013-11-16 05:51:38 -05:00
snipe 0894832e60 Cleanup, adding model CRUD 2013-11-16 03:55:18 -05:00
snipe 2e132331e7 Refactoring
Removes extraneous create.blade.php, cleaned up some functions
2013-11-16 03:25:40 -05:00
snipe 4f0a1789db Fixed license blades 2013-11-16 01:14:52 -05:00
snipe 23a6e867e6 Math is hard. :(
Some ghetto straight-line depreciation shit happening here. Assumign 30 days in a month, blah blah blah. Will need a better fix later, but this is fine for MVP
2013-11-16 01:09:21 -05:00
snipe 9720ff9fd5 Added (non-functional) search field 2013-11-15 23:46:20 -05:00
snipe 32eb9d1d30 Added depreciations 2013-11-15 23:46:03 -05:00
snipe 31b90cf2a3 removed ID column. No one cares about the ID 2013-11-15 22:33:02 -05:00
snipe e475846fb6 nav lockout for non-logged in users 2013-11-15 22:27:13 -05:00
snipe 95750cb3a9 Added auth check in header 2013-11-15 22:08:48 -05:00
snipe 86087b2c4f merged fronted with backend 2013-11-15 22:08:36 -05:00
snipe 9c3be972f0 Skip all the public crap. Login or GTFO 2013-11-15 22:01:19 -05:00
snipe 6a70acbe5f sexy calendar date picker icon 2013-11-15 22:01:01 -05:00
snipe f60cba1a4a Added date picker for purchase date 2013-11-15 21:32:54 -05:00
snipe 38154c0df8 typo 2013-11-15 21:31:33 -05:00
snipe 3af94b4e7e Added model drop down 2013-11-15 21:30:48 -05:00
snipe 4cd458cc7a Minor column edits 2013-11-15 19:21:20 -05:00
snipe ccf59fb79d updated url with asset link
this link is dumb and I will probably fix it later
2013-11-15 19:20:49 -05:00
snipe 7e840ec273 Asset view blades 2013-11-15 19:20:26 -05:00
snipe 8b57cd8ab9 Updated routes with assets 2013-11-15 19:20:04 -05:00
snipe 0839510807 Placeholder asset model 2013-11-15 19:19:54 -05:00
snipe b0eb5e5bdd Language files for messages + index table 2013-11-15 19:19:41 -05:00
snipe f12e6b0837 Added DB 2013-11-15 19:18:48 -05:00
snipe 16143c4ff7 New asset controller 2013-11-15 19:18:28 -05:00
snipe 40713d04f9 Add/Edit/View blades 2013-11-15 15:48:30 -05:00
snipe 2a08e00a16 License routes 2013-11-15 15:48:11 -05:00
snipe 5e6a22d973 Added currency 2013-11-15 15:48:00 -05:00
snipe f27fa40f1c Added license name 2013-11-15 15:47:47 -05:00
snipe abd060679e License CRUD 2013-11-15 15:47:34 -05:00
snipe 34cb25296f Added purchase date, etc to language files 2013-11-15 15:47:02 -05:00
snipe 9bbc25fc76 License language files 2013-11-15 15:00:50 -05:00
snipe e9fe59444f Placeholder licenses model 2013-11-15 14:59:50 -05:00
snipe 9364c46c2d Fixed licensed link 2013-11-15 14:59:35 -05:00
snipe 50dfa27689 Licenses route 2013-11-15 14:59:03 -05:00
snipe d7dedf84ad Added assets column to index blade 2013-11-15 14:28:07 -05:00
snipe fd0e5959b5 Nav tweaks
Still working on the info architecture of this - added licenses, and removed users, as users will go under People
2013-11-15 14:27:43 -05:00
snipe ba8cf36691 Added assets column 2013-11-15 14:26:35 -05:00
snipe 5a46c85533 asset count per user.
this shit is very broken right now.
2013-11-15 14:25:57 -05:00
snipe 9159d5f771 Added license/asset tables 2013-11-15 14:25:28 -05:00
snipe c9293897fb manufacturer CRUD 2013-11-15 06:39:58 -05:00
snipe 0e4e26206b fixed broken redirect route 2013-11-15 06:39:07 -05:00
snipe 884131f729 added user_id to manufacturer table 2013-11-15 06:38:50 -05:00
snipe 1f2badfc39 minor UI tweaks
Added icons to action buttons, removed ability to edit a deleted user
2013-11-15 06:20:06 -05:00
snipe 79da13dff7 Fixed layout for category view 2013-11-15 05:07:34 -05:00
snipe b8d4823cae Removed created_at column in results.
No one cares when a category or manufacturer was created.
2013-11-15 05:06:26 -05:00
snipe 0789108e74 Added Trello board URL 2013-11-15 04:59:36 -05:00
snipe c591a71141 only show pagination if results > 10 2013-11-15 04:46:41 -05:00
snipe 2b930ff1e7 minor UI flow changes, disallow a category to be its own parent 2013-11-15 04:34:10 -05:00
snipe af03d60d28 Copy tweak 2013-11-15 04:30:49 -05:00
snipe 51cd0b0208 only paginate if there are > 10 results, added parentname output 2013-11-15 04:30:38 -05:00
snipe e5aa5f1a5b Added parentname method to get the name of the parent category for display 2013-11-15 04:30:05 -05:00
snipe 1bd87cca05 removed excess white space 2013-11-15 04:27:26 -05:00
snipe 0fcd64c386 categories dropdown in create/edit form 2013-11-14 22:32:16 -05:00
snipe 590dbb2bd5 Create form
I will probably make these modals later - the UX is a little clumsy this way
2013-11-14 21:46:13 -05:00
snipe 54cd3d04b1 Added buttons for edit/delete 2013-11-14 21:45:33 -05:00
snipe 6218e1912f Added edit/delete routes for categories 2013-11-14 21:45:01 -05:00
snipe 14972f894b fixed typo 2013-11-14 21:44:51 -05:00
snipe cf1134db37 Edit/Delete categories 2013-11-14 21:44:42 -05:00
snipe 963d3a764a adding user ID to category table 2013-11-14 21:44:19 -05:00
snipe 07e71a547c controller cleanup 2013-11-13 17:49:07 -05:00
snipe 98afb39ef0 manufacturer's nav 2013-11-13 17:48:54 -05:00
snipe 5d3d1eb319 manufacturers language files 2013-11-13 17:48:37 -05:00
snipe a1f9296700 new route for manufacturers 2013-11-13 17:47:26 -05:00
snipe a8d192d263 updated category view 2013-11-13 17:47:16 -05:00
snipe 7785743d85 new manufacturers views 2013-11-13 17:46:55 -05:00
snipe 4ac4b2fde0 Added model number to blades 2013-11-13 17:46:37 -05:00
snipe f387f7e005 spelling is really hard :( 2013-11-13 17:29:28 -05:00
snipe 721a858aa3 Added categories to router 2013-11-13 17:04:05 -05:00
snipe b3c85cd554 rough category model 2013-11-13 17:02:48 -05:00
snipe 760c41896e first manufacturer controller 2013-11-13 17:02:37 -05:00
snipe b18f69330f language files for category view 2013-11-13 17:02:25 -05:00
snipe e08741b104 created category controller 2013-11-13 17:02:11 -05:00
snipe dd8d23bcf7 category view blades 2013-11-13 17:01:52 -05:00
snipe 1ff564ad9f updated nav 2013-11-13 17:01:33 -05:00
snipe f7466a335d nav additions 2013-11-13 16:23:36 -05:00
snipe 261049e848 placeholder model/category models 2013-11-13 16:23:18 -05:00
snipe 9747815960 license/contribution info 2013-11-13 16:22:59 -05:00
snipe 4c9b66150d added travis-ci yml 2013-11-13 15:24:25 -05:00
snipe 41efea5929 Updated read me 2013-11-13 14:45:48 -05:00
snipe 52acfafc42 updated read me 2013-11-13 14:44:11 -05:00
snipe 5721636b71 minor change to readme 2013-11-13 05:57:03 -05:00
snipe 01dadedc78 Rough model view blades 2013-11-13 05:56:24 -05:00
snipe 6bca4fa187 Rough scaffold for models controller 2013-11-13 05:56:05 -05:00
snipe 92f80f979f Adding english language phrases for the mode display table 2013-11-13 05:54:57 -05:00
snipe fc6e41e464 Removing extraneous nav
This view/nav will probably go away, since we don't really have a frontend, everything is behind a login
2013-11-13 05:54:23 -05:00
snipe 574822f278 modified nav 2013-11-13 05:53:30 -05:00
snipe 91b85de6cf New routes for new sections 2013-11-13 05:53:14 -05:00
snipe dd20f519b5 DB seeds 2013-11-13 05:50:20 -05:00
snipe 6f80a2dedd Default DB creds 2013-11-13 05:49:22 -05:00
snipe 53ca5e9c5d Create models/categories/manufacturer table migrations 2013-11-13 05:49:03 -05:00
snipe feda21a6e3 Initial laravel import 2013-11-13 01:59:39 -05:00
snipe 87e90f12ea Initial commit 2013-11-12 14:25:47 -08:00
7364 changed files with 77674 additions and 827494 deletions
-3141
View File
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
.git
.github
.gitattributes
.gitignore
.dockerignore
app/storage/logs/*
app/storage/views/*
vendor/*
storage/framework/cache/*
node_modules
.vagrant
.idea
-166
View File
@@ -1,166 +0,0 @@
# --------------------------------------------
# REQUIRED: DB SETUP
# --------------------------------------------
MYSQL_DATABASE=snipeit
MYSQL_USER=snipeit
MYSQL_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=develop
APP_DEBUG=false
# please regenerate the APP_KEY value by calling `docker-compose run --rm snipeit bash` and then `php artisan key:generate --show` and then copy paste the value here
APP_KEY=base64:3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ=
APP_URL=http://localhost:8000
APP_TIMEZONE='UTC'
APP_LOCALE=en
MAX_RESULTS=500
# --------------------------------------------
# REQUIRED: UPLOADED FILE STORAGE SETTINGS
# --------------------------------------------
PRIVATE_FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=mariadb
DB_DATABASE=snipeit
DB_USERNAME=snipeit
DB_PASSWORD=changeme1234
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS
# --------------------------------------------
DB_SSL=false
DB_SSL_IS_PAAS=false
DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_DRIVER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME='Snipe-IT'
MAIL_REPLYTO_ADDR=you@example.com
MAIL_REPLYTO_NAME='Snipe-IT'
MAIL_AUTO_EMBED_METHOD='attachment'
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: BACKUP SETTINGS
# --------------------------------------------
MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false
ENCRYPT=false
COOKIE_NAME=snipeit_session
COOKIE_DOMAIN=null
SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=40
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=false
CORS_ALLOWED_ORIGINS=null
ENABLE_HSTS=false
# --------------------------------------------
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
# OPTIONAL: REDIS SETTINGS
# --------------------------------------------
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
# --------------------------------------------
# OPTIONAL: MEMCACHED SETTINGS
# --------------------------------------------
MEMCACHED_HOST=null
MEMCACHED_PORT=null
# --------------------------------------------
# OPTIONAL: PUBLIC S3 Settings
# --------------------------------------------
PUBLIC_AWS_SECRET_ACCESS_KEY=null
PUBLIC_AWS_ACCESS_KEY_ID=null
PUBLIC_AWS_DEFAULT_REGION=null
PUBLIC_AWS_BUCKET=null
PUBLIC_AWS_URL=null
PUBLIC_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: PRIVATE S3 Settings
# --------------------------------------------
PRIVATE_AWS_ACCESS_KEY_ID=null
PRIVATE_AWS_SECRET_ACCESS_KEY=null
PRIVATE_AWS_DEFAULT_REGION=null
PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null
PRIVATE_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: AWS Settings
# --------------------------------------------
AWS_ACCESS_KEY_ID=null
AWS_SECRET_ACCESS_KEY=null
AWS_DEFAULT_REGION=null
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60
RESET_PASSWORD_LINK_EXPIRES=900
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=stderr
LOG_MAX_DAYS=10
APP_LOCKED=false
APP_CIPHER=AES-256-CBC
APP_FORCE_TLS=false
GOOGLE_MAPS_API=
LDAP_MEM_LIM=500M
LDAP_TIME_LIM=600
-172
View File
@@ -1,172 +0,0 @@
# --------------------------------------------
# REQUIRED: DOCKER SPECIFIC SETTINGS
# --------------------------------------------
APP_VERSION=v6.4.1
APP_PORT=8000
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=production
APP_DEBUG=false
# Please regenerate the APP_KEY value by calling `docker compose run --rm snipeit php artisan key:generate --show`. Copy paste the value here
APP_KEY=base64:3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ=
APP_URL=http://localhost:8000
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - TZ identifier
APP_TIMEZONE='UTC'
APP_LOCALE=en
MAX_RESULTS=500
# --------------------------------------------
# REQUIRED: UPLOADED FILE STORAGE SETTINGS
# --------------------------------------------
PRIVATE_FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT='3306'
DB_DATABASE=snipeit
DB_USERNAME=snipeit
DB_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS
# --------------------------------------------
DB_SSL=false
DB_SSL_IS_PAAS=false
DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_TLS_VERIFY_PEER=true
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME='Snipe-IT'
MAIL_REPLYTO_ADDR=you@example.com
MAIL_REPLYTO_NAME='Snipe-IT'
MAIL_AUTO_EMBED_METHOD='attachment'
# --------------------------------------------
# REQUIRED: DATA PROTECTION
# --------------------------------------------
ALLOW_BACKUP_DELETE=false
ALLOW_DATA_PURGE=false
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: BACKUP SETTINGS
# --------------------------------------------
MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false
ENCRYPT=false
COOKIE_NAME=snipeit_session
COOKIE_DOMAIN=null
SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=40
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1,172.0.0.0/8
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=false
CORS_ALLOWED_ORIGINS=null
ENABLE_HSTS=false
# --------------------------------------------
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
# OPTIONAL: REDIS SETTINGS
# --------------------------------------------
REDIS_HOST=null
REDIS_PASSWORD=null
REDIS_PORT=6379
# --------------------------------------------
# OPTIONAL: MEMCACHED SETTINGS
# --------------------------------------------
MEMCACHED_HOST=null
MEMCACHED_PORT=null
# --------------------------------------------
# OPTIONAL: PUBLIC S3 Settings
# --------------------------------------------
PUBLIC_AWS_SECRET_ACCESS_KEY=null
PUBLIC_AWS_ACCESS_KEY_ID=null
PUBLIC_AWS_DEFAULT_REGION=null
PUBLIC_AWS_BUCKET=null
PUBLIC_AWS_URL=null
PUBLIC_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: PRIVATE S3 Settings
# --------------------------------------------
PRIVATE_AWS_ACCESS_KEY_ID=null
PRIVATE_AWS_SECRET_ACCESS_KEY=null
PRIVATE_AWS_DEFAULT_REGION=null
PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null
PRIVATE_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: AWS Settings
# --------------------------------------------
AWS_ACCESS_KEY_ID=null
AWS_SECRET_ACCESS_KEY=null
AWS_DEFAULT_REGION=null
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60
RESET_PASSWORD_LINK_EXPIRES=900
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=stderr
LOG_MAX_DAYS=10
APP_LOCKED=false
APP_CIPHER=AES-256-CBC
APP_FORCE_TLS=false
GOOGLE_MAPS_API=
LDAP_MEM_LIM=500M
LDAP_TIME_LIM=600
-107
View File
@@ -1,107 +0,0 @@
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=local
APP_DEBUG=false
APP_KEY=base64:hTUIUh9CP6dQx+6EjSlfWTgbaMaaRvlpEwk45vp+xmk=
APP_URL=http://127.0.0.1:8000
APP_TIMEZONE='US/Eastern'
APP_LOCALE=en
APP_LOCKED=false
MAX_RESULTS=200
# --------------------------------------------
# REQUIRED: UPLOADED FILE STORAGE SETTINGS
# --------------------------------------------
PRIVATE_FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=null
DB_USERNAME=null
DB_PASSWORD=null
DB_PREFIX=null
#DB_DUMP_PATH=
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS
# --------------------------------------------
DB_SSL=false
DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_MAILER="log"
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false
ENCRYPT=true
COOKIE_NAME=snipeit_v5_local
SECURE_COOKIES=true
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
REFERRER_POLICY=same-origin
ENABLE_CSP=true
CORS_ALLOWED_ORIGINS="*"
# --------------------------------------------
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=50000
LOGIN_LOCKOUT_DURATION=1000
RESET_PASSWORD_LINK_EXPIRES=15
# --------------------------------------------
# OPTIONAL: API
# --------------------------------------------
API_MAX_REQUESTS_PER_HOUR=200
# --------------------------------------------
# OPTIONAL: SAML SETTINGS
# --------------------------------------------
DISABLE_NOSAML_LOCAL_LOGIN=true
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=single
LOG_LEVEL=debug
LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=null
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=true
ENABLE_HSTS=false
WARN_DEBUG=false
APP_CIPHER=AES-256-CBC
-200
View File
@@ -1,200 +0,0 @@
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=production
APP_DEBUG=false
APP_KEY=ChangeMe
APP_URL=null
APP_TIMEZONE='UTC'
APP_LOCALE='en-US'
MAX_RESULTS=500
# --------------------------------------------
# REQUIRED: UPLOADED FILE STORAGE SETTINGS
# --------------------------------------------
PRIVATE_FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=local_public
#PRIVATE_FILESYSTEM_DISK=s3_private
#PUBLIC_FILESYSTEM_DISK=s3_public
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=null
DB_USERNAME=null
DB_PASSWORD=null
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS
# --------------------------------------------
DB_SSL=false
DB_SSL_IS_PAAS=false
DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_MAILER=smtp
MAIL_HOST=email-smtp.us-west-2.amazonaws.com
MAIL_PORT=587
MAIL_USERNAME=YOURUSERNAME
MAIL_PASSWORD=YOURPASSWORD
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME='Snipe-IT'
MAIL_REPLYTO_ADDR=you@example.com
MAIL_REPLYTO_NAME='Snipe-IT'
MAIL_AUTO_EMBED_METHOD='attachment'
MAIL_TLS_VERIFY_PEER=true
# MAIL_ENCRYPTION is no longer supported. SymfonyMailer will use tls if it's
# advertised, and won't if it's not. If you want to use your mail server's IP but it's failing
# because of certificate errors, set MAIL_TLS_VERIFY_PEER-true
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: BACKUP SETTINGS
# --------------------------------------------
MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
ALLOW_BACKUP_DELETE=false
ALLOW_DATA_PURGE=false
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
SESSION_DRIVER=file
SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false
ENCRYPT=false
COOKIE_NAME=snipeit_session
COOKIE_DOMAIN=null
SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=15
BS_TABLE_STORAGE=cookieStorage
BS_TABLE_DEEPLINK=true
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=false
ADDITIONAL_CSP_URLS=null
CORS_ALLOWED_ORIGINS=null
ENABLE_HSTS=false
# --------------------------------------------
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
# OPTIONAL: REDIS SETTINGS
# --------------------------------------------
REDIS_HOST=null
REDIS_PASSWORD=null
REDIS_PORT=null
# --------------------------------------------
# OPTIONAL: MEMCACHED SETTINGS
# --------------------------------------------
MEMCACHED_HOST=null
MEMCACHED_PORT=null
# --------------------------------------------
# OPTIONAL: PUBLIC S3 Settings
# --------------------------------------------
PUBLIC_AWS_SECRET_ACCESS_KEY=null
PUBLIC_AWS_ACCESS_KEY_ID=null
PUBLIC_AWS_DEFAULT_REGION=null
PUBLIC_AWS_BUCKET=null
PUBLIC_AWS_URL=null
PUBLIC_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: PRIVATE S3 Settings
# --------------------------------------------
PRIVATE_AWS_ACCESS_KEY_ID=null
PRIVATE_AWS_SECRET_ACCESS_KEY=null
PRIVATE_AWS_DEFAULT_REGION=null
PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null
PRIVATE_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: AWS Settings
# --------------------------------------------
AWS_ACCESS_KEY_ID=null
AWS_SECRET_ACCESS_KEY=null
AWS_DEFAULT_REGION=null
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60
LOGIN_AUTOCOMPLETE=false
# --------------------------------------------
# OPTIONAL: FORGOTTEN PASSWORD SETTINGS
# --------------------------------------------
RESET_PASSWORD_LINK_EXPIRES=15
PASSWORD_CONFIRM_TIMEOUT=10800
PASSWORD_RESET_MAX_ATTEMPTS_PER_MIN=50
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=single
LOG_MAX_DAYS=10
APP_LOCKED=false
APP_CIPHER=AES-256-CBC
APP_FORCE_TLS=false
APP_ALLOW_INSECURE_HOSTS=false
GOOGLE_MAPS_API=
LDAP_MEM_LIM=500M
LDAP_TIME_LIM=600
IMPORT_TIME_LIMIT=600
IMPORT_MEMORY_LIMIT=500M
REPORT_TIME_LIMIT=12000
REQUIRE_SAML=false
API_THROTTLE_PER_MINUTE=120
CSV_ESCAPE_FORMULAS=true
# --------------------------------------------
# OPTIONAL: HASHING
# --------------------------------------------
HASHING_DRIVER='bcrypt'
BCRYPT_ROUNDS=10
ARGON_MEMORY=1024
ARGON_THREADS=2
ARGON_TIME=2
# --------------------------------------------
# OPTIONAL: SCIM
# --------------------------------------------
SCIM_TRACE=false
SCIM_STANDARDS_COMPLIANCE=false
-38
View File
@@ -1,38 +0,0 @@
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV='testing-ci'
APP_DEBUG=false
APP_KEY='base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU='
APP_URL='http://localhost:8000'
APP_TIMEZONE='US/Pacific'
APP_LOCALE='en-US'
FILESYSTEM_DISK=local
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=sqlite
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE='sqlite_testing'
DB_USERNAME=root
DB_PASSWORD=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_MAILER=log
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: APP LOG FORMAT
# --------------------------------------------
LOG_CHANNEL=single
-19
View File
@@ -1,19 +0,0 @@
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=testing
APP_DEBUG=true
APP_KEY=base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU=
APP_URL=http://localhost:8000
APP_TIMEZONE='UTC'
APP_LOCALE='en-US'
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=null
DB_USERNAME=null
DB_PASSWORD=null
-23
View File
@@ -1,23 +0,0 @@
APP_ENV=testing
APP_DEBUG=true
APP_URL=http://snipe-it.localapp
DB_CONNECTION=mysql
DB_DEFAULT=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=snipeittests
DB_USERNAME=snipeit
DB_PASSWORD=snipe
APP_KEY=base64:tu9NRh/a6+dCXBDGvg0Gv/0TcABnFsbT4AKxrr8mwQo=
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# (LOGIN_LOCKOUT_DURATIONin minutes)
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=1000000
LOGIN_LOCKOUT_DURATION=100000000
MAIL_MAILER=log
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME=Snipe-IT
-20
View File
@@ -1,20 +0,0 @@
APP_ENV=testing
APP_DEBUG=true
APP_URL=http://snipe-it.localapp
DB_CONNECTION=sqlite_testing
DB_DEFAULT=sqlite_testing
DB_HOST=localhost
DB_PORT=3306
APP_KEY=base64:tu9NRh/a6+dCXBDGvg0Gv/0TcABnFsbT4AKxrr8mwQo=
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# (LOGIN_LOCKOUT_DURATIONin minutes)
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=1000000
LOGIN_LOCKOUT_DURATION=100000000
MAIL_MAILER=log
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME=Snipe-IT
+1 -3
View File
@@ -1,3 +1 @@
* text=auto
public/js/** binary
public/css/** binary
* text=auto
-22
View File
@@ -1,22 +0,0 @@
# Code ownership for pull request reviews, per
# the feature detailed here:
# https://github.com/blog/2392-introducing-code-owners
# https://help.github.com/articles/about-codeowners/
# These owners will be the default owners for everything in the repo.
* @snipe
# Order is important. The last matching pattern has the most precedence.
# So if a pull request only touches javascript files, only these owners
# will be requested to review.
# For example:
# *.js @octocat @github/js
app/Http/Controllers/CustomFields* @uberbrady
app/Http/Controllers/Api/CustomFields* @uberbrady
resources/views/custom_fields/* @uberbrady
docker/* @uberbrady
app/Providers/SamlServiceProvider.php @uberbrady
-5
View File
@@ -1,5 +0,0 @@
# You can add one username per supported platform and one custom link
# patreon: # Replace with your Patreon username
# open_collective: # Replace with your Open Collective username
# ko_fi: # Replace with your Ko-fi username
custom: https://snipeitapp.com/donate
-38
View File
@@ -1,38 +0,0 @@
#### Expected Behavior (or desired behavior if a feature request)
(what you expect to happen goes here)
-----
#### Actual Behavior
(what actually happens goes here)
-----
#### Please confirm you have done the following before posting your bug report:
- [ ] I have enabled debug mode
- [ ] I have read [checked the Common Issues page](https://snipe-it.readme.io/docs/common-issues)
-----
#### Provide answers to these questions:
- Is this a fresh install or an upgrade?
- Version of Snipe-IT you're running
- Version of PHP you're running
- Version of MySQL/MariaDB you're running
- What OS and web server you're running Snipe-IT on
- What method you used to install Snipe-IT (install.sh, manual installation, docker, etc)
- WITH DEBUG TURNED ON, if you're getting an error in your browser, include that error
- What specific Snipe-IT page you're on, and what specific element you're interacting with to trigger the error
- If a stacktrace is provided in the error, include that too.
- Any errors that appear in your browser's error console.
- Confirm whether the error is reproducible on the demo: https://snipeitapp.com/demo.
- Include any additional information you can find in `storage/logs` and your webserver's logs.
- Include what you've done so far in the installation, and if you got any error messages along the way.
- Indicate whether or not you've manually edited any data directly in the database
Please do not post an issue without answering the related questions above. If you have opened a different issue and already answered these questions, answer them again, once for every ticket. It will be next to impossible for us to help you.
https://snipe-it.readme.io/docs/getting-help
-129
View File
@@ -1,129 +0,0 @@
name: Bug Report
description: Create a report to help us improve
body:
- type: checkboxes
attributes:
label: Debug mode
description: Please confirm you have done the following before posting your bug report
options:
- label: I have enabled debug mode
required: true
- label: I have read [checked the Common Issues page](https://snipe-it.readme.io/docs/common-issues)
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: Steps to reproduce the behavior.
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Screenshots
description: 'If applicable, add screenshots to help explain your problem.'
- type: markdown
attributes:
value: "### Server"
- type: input
attributes:
label: Snipe-IT Version
validations:
required: true
- type: input
id: server_operatingSystem
attributes:
label: Operating System
description: 'e.g. Ubuntu, Windows'
validations:
required: true
- type: input
attributes:
label: Web Server
description: 'e.g. Apache, IIS'
validations:
required: true
- type: input
attributes:
label: PHP Version
validations:
required: true
- type: markdown
attributes:
value: "### Desktop"
- type: input
id: desktop_operatingSystem
attributes:
label: Operating System
description: 'e.g. Ubuntu, Windows'
- type: input
id: desktop_browser
attributes:
label: Browser
description: 'e.g. Google Chrome, Safari'
- type: input
id: desktop_version
attributes:
label: Version
description: 'e.g. 93'
- type: markdown
attributes:
value: "### Mobile"
- type: input
attributes:
label: Device
description: 'e.g. iPhone 6, Pixel 4a'
- type: input
id: mobile_operatingSystem
attributes:
label: Operating System
description: 'e.g. iOS 8.1, Android 9'
- type: input
id: mobile_browser
attributes:
label: Browser
description: 'e.g. Google Chrome, Safari'
- type: input
id: mobile_version
attributes:
label: Version
description: 'e.g. 93'
- type: textarea
attributes:
label: Error messages
description: |
WITH DEBUG TURNED ON, if you're getting an error in your browser, include that error
If a stacktrace is provided in the error, include that too.
Any errors that appear in your browser's error console.
Confirm whether the error is reproducible on the demo: https://snipeitapp.com/demo.
Include any additional information you can find in `storage/logs` and your webserver's logs.
Include the output from `php -m` (this should display what modules you have enabled.)
render: shell
- type: textarea
attributes:
label: Additional context
description: |
Is this a fresh install or an upgrade?
What OS and web server you're running Snipe-IT on
What method you used to install Snipe-IT (install.sh, manual installation, docker, etc)
Include what you've done so far in the installation, and if you got any error messages along the way.
Indicate whether or not you've manually edited any data directly in the database
Add any other context about the problem here.
- type: markdown
attributes:
value: Please do not post an issue without answering the related questions above. If you have opened a different issue and already answered these questions, answer them again, once for every ticket. It will be next to impossible for us to help you.
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
@@ -1,25 +0,0 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature Request]: "
labels: ["feature request"]
body:
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. The more information you can provide about your use-case, the more liklely we are to consider your feature.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
-22
View File
@@ -1,22 +0,0 @@
frontend: ["*.js", "*.css", "*.vue", "*.scss", "*.less", "*.blade.*", "resources/views/livewire/*"]
skins: ["*.js", "*.css", "*.scss", "*.less"]
css: ["*.css","*.scss", "*.less"]
javascript: ["*.js", "package.json", "package.lock"]
backend: ["/app/*", "composer.json", "composer.lock"]
translations: ["/resources/lang"]
livewire: ["/app/Http/Livewire/*", "resources/views/livewire/*"]
backups: ["*backup*"]
restore: ["*restore*"]
saml: ["*saml*"]
scim: ["*scim*"]
custom fields: ["*fields*", "*fieldsets*"]
dependencies: ["composer.json", "composer.lock", "package.json", "package.lock"]
consumables: ["*consumables*"]
api: ["/app/Http/Controllers/Api/*"]
notifications: ["/app/Notifications/*"]
importer: ["/app/Importer/*","/app/Http/Livewire/Importer.php", "resources/views/livewire/importer.php"]
cli / artisan: ["/app/Console/*"]
LDAP: ["*Ldap*", "/app/Console/Commands/Ldap*","/app/Models/Ldap.php"]
docker: ["*docker/*", "Dockerfile", "Dockerfile.alpine", "Dockerfile.fpm-alpine", ".dockerignore", ".env.docker"]
tests: ["/tests/*", "/database/factories/*", "/stubs"]
config: .github
-33
View File
@@ -1,33 +0,0 @@
# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
# Comment to be posted to on first time issues
newIssueWelcomeComment: |
👋 Thanks for opening your first issue here! If you're reporting a 🐞 bug, please make sure you include steps to reproduce it. We get a lot of issues on this repo, so please be patient and we will get back to you as soon as we can.
# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: |
💖 Thanks for this pull request! 💖
We use [semantic commit messages](https://snipe-it.readme.io/docs/contributing-overview#section-pull-request-guidelines) to streamline the release process and easily generate changelogs between versions. Before your pull request can be merged, you should **update your pull request title** to start with a semantic prefix if it doesn't have one already.
Examples of commit messages with semantic prefixes:
- `Fixed #<issue number>: don't overwrite prevent_default if default wasn't prevented`
- `Added #<issue number>: add checkout functionality to assets`
- `Improved Asset Checkout: use new notification method for checkout`
Things that will help get your PR across the finish line:
- Document any user-facing changes you've made.
- Include tests when adding/changing behavior.
- Include screenshots and animated GIFs whenever possible.
We get a lot of pull requests on this repo, so please be patient and we will get back to you as soon as we can.
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request! 🎉🎉🎉
-7
View File
@@ -1,7 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "develop"
schedule:
interval: "weekly"
-40
View File
@@ -1,40 +0,0 @@
# Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context, providing screenshots where practical. List any dependencies that are required for this change.
Fixes # (issue)
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update
# How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
- [ ] Test A
- [ ] Test B
**Test Configuration**:
* PHP version:
* MySQL version
* Webserver version
* OS version
# Checklist:
- [ ] I have read the Contributing documentation available here: https://snipe-it.readme.io/docs/contributing-overview
- [ ] I have formatted this PR according to the project guidelines: https://snipe-it.readme.io/docs/contributing-overview#pull-request-guidelines
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
-43
View File
@@ -1,43 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- :woman_technologist: ready for dev
- :moneybag: bounty
- :hand: bug
- "🔐 security"
- "👩‍💻 ready for dev"
- "💰 bounty"
- "✋ bug"
exemptMilestones: true
# Label to use when marking an issue as stale
staleLabel: stale
only: issues
# Comment to post when removing the stale label.
unmarkComment: >
Okay, it looks like this issue or feature request might still be important. We'll re-open
it for now. Thank you for letting us know!
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
Is this still relevant? We haven't heard from anyone in a bit. If so,
please comment with any updates or additional detail.
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Don't
take it personally, we just need to keep a handle on things. Thank you
for your contributions!
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue has been automatically closed because it has not had
recent activity. If you believe this is still an issue, please confirm that
this issue is still happening in the most recent version of Snipe-IT and reply
to this thread to re-open it.
-1
View File
@@ -1 +0,0 @@
memory_limit= 2048M
-7
View File
@@ -1,7 +0,0 @@
# Configuration for weekly-digest - https://github.com/apps/weekly-digest
publishDay: sun
canPublishIssues: true
canPublishPullRequests: true
canPublishContributors: true
canPublishStargazers: true
canPublishCommits: true
-39
View File
@@ -1,39 +0,0 @@
# This workflow checks out code, performs a CodeQL analysis (for JavaScript) and integrates the results
# with the GitHub Advanced Security code scanning feature.
# More information: https://codeql.github.com/
name: CodeQL Security Scan
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
# schedule:
# - cron: '15 17 * * 1'
jobs:
analyze:
name: CodeQL Security Scan
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
-57
View File
@@ -1,57 +0,0 @@
# This workflow checks out code, performs a Codacy security scan
# and integrates the results with the
# GitHub Advanced Security code scanning feature. For more information on
# the Codacy security scan action usage and parameters, see
# https://github.com/codacy/codacy-analysis-cli-action.
# For more information on Codacy Analysis CLI in general, see
# https://github.com/codacy/codacy-analysis-cli.
name: Codacy Security Scan
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '36 23 * * 3'
permissions:
contents: read
jobs:
codacy-security-scan:
# Ensure schedule job never runs on forked repos. It's only executed for 'snipe/snipe-it'
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
if: (github.repository == 'snipe/snipe-it') || ((github.repository != 'snipe/snipe-it') && (github.event_name != 'schedule'))
name: Codacy Security Scan
runs-on: ubuntu-latest
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout code
uses: actions/checkout@v4
# 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.1
with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
verbose: true
output: results.sarif
format: sarif
# Adjust severity of non-security issues
gh-code-scanning-compat: true
# Force 0 exit code to allow SARIF file generation
# This will handover control about PR rejection to the GitHub side
max-allowed-issues: 2147483647
# Upload the SARIF file generated in the previous step
- name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
-21
View File
@@ -1,21 +0,0 @@
name: Crowdin Action
on:
push:
branches: [ develop ]
jobs:
upload-sources-to-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Crowdin push
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: false
download_translations: false
project_id: ${{ secrets.CROWDIN_PROJECT_ID }}
token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
-86
View File
@@ -1,86 +0,0 @@
# Snipe-IT (Alpine) Docker image build for hub.docker.com
name: Docker images (Alpine)
# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
# Also run for PRs to ensure PR doesn't break Docker build process
on:
push:
branches:
- master
- develop
tags:
- 'v**'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
pull_request:
permissions:
contents: read
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
if: github.repository == 'snipe/snipe-it'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (master), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }},suffix=-alpine
type=ref,event=tag,suffix=-alpine
type=semver,pattern=v{{major}}-latest-alpine
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v4
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'snipe/snipe-it' image
###############################################
# https://github.com/docker/metadata-action
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build
uses: docker/metadata-action@v5
with:
images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.alpine
platforms: linux/amd64,linux/arm64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
# Use tags / labels provided by 'docker/metadata-action' above
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}
-86
View File
@@ -1,86 +0,0 @@
# Snipe-IT Docker image build for hub.docker.com
name: Docker images
# Run this Build for all pushes to 'master' or develop branch, or tagged releases.
# Also run for PRs to ensure PR doesn't break Docker build process
on:
push:
branches:
- master
- develop
tags:
- 'v**'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
pull_request:
permissions:
contents: read
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'snipe/snipe-it'
if: github.repository == 'snipe/snipe-it'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (master), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
type=semver,pattern=v{{major}}-latest
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v4
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'snipe/snipe-it' image
###############################################
# https://github.com/docker/metadata-action
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'snipe-it' image
id: meta_build
uses: docker/metadata-action@v5
with:
images: snipe/snipe-it
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
# Use tags / labels provided by 'docker/metadata-action' above
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}
@@ -1,22 +0,0 @@
name: Update Docker Hub Description
on:
push:
branches:
- master
- develop
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker Hub Description
uses: grokability/dockerhub-description@7ea9d275c7cdbe2b676a093a0308c50665e3b8b4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
repository: snipe/snipe-it
readme-filepath: ./README.md
-79
View File
@@ -1,79 +0,0 @@
name: Tests in MySQL
on:
push:
branches:
- master
- develop
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: snipeit
ports:
- 33306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
fail-fast: false
matrix:
php-version:
- "8.1"
- "8.2"
- "8.3"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Copy .env
run: |
cp -v .env.testing.example .env
cp -v .env.testing.example .env.testing
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Setup Laravel
env:
DB_CONNECTION: mysql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USERNAME: root
run: |
php artisan key:generate
php artisan migrate --force
php artisan passport:install
chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: mysql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.mysql.ports[3306] }}
DB_USERNAME: root
run: php artisan test --parallel
-75
View File
@@ -1,75 +0,0 @@
name: Tests in Postgres
on: workflow_dispatch
jobs:
tests:
runs-on: ubuntu-latest
services:
postgresql:
image: postgres
env:
POSTGRES_DB: snipeit
POSTGRES_USER: snipeit
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
fail-fast: false
matrix:
php-version:
- "8.2"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Copy .env
run: |
cp -v .env.testing.example .env
cp -v .env.testing.example .env.testing
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Setup Laravel
env:
DB_CONNECTION: pgsql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
DB_USERNAME: snipeit
DB_PASSWORD: password
run: |
php artisan key:generate
php artisan migrate --force
php artisan passport:install
chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: pgsql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
DB_USERNAME: snipeit
DB_PASSWORD: password
run: php artisan test --parallel
-61
View File
@@ -1,61 +0,0 @@
name: Tests in SQLite
on:
push:
branches:
- master
- develop
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-version:
- "8.1.1"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Copy .env
run: |
cp -v .env.testing.example .env
cp -v .env.testing.example .env.testing
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Setup Passport
run: php artisan passport:keys
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite_testing
run: php artisan test --parallel
+8 -66
View File
@@ -1,69 +1,11 @@
.couscous
.DS_Store
.env
.env.testing
phpstan.neon
.idea
/bin/
/bootstrap/compiled.php
/node_modules
/vendor
app/database/*.sqlite
app/storage/meta/services.json
composer.phar
crowdin.yaml
Homestead.json
Homestead.yaml
output
phpDocumentor.phar
public/uploads/*.gif
public/uploads/barcodes/*.png
public/uploads/*.jpg
public/uploads/*.png
public/uploads/*.svg
public/uploads/*.tif
public/uploads/assets/*
public/uploads/avatars/*
public/uploads/logo.gif
public/uploads/logo.png
public/uploads/logo.svg
public/uploads/models/*
public/uploads/suppliers/*
public/uploads/accessories/*
public/uploads/locations/*
public/uploads/manufacturers/*
public/uploads/components/*
public/uploads/consumables/*
public/uploads/companies/*
public/uploads/categories/*
public/uploads/users/*
storage/app/private_uploads/users/*
public/uploads/departments/*
storage/debugbar/
storage/dumps/*
storage/laravel-backups
storage/logs/*
storage/private_uploads/users/*
tests/_data/scenarios
tests/_output/*
tests/_support/_generated/*
/npm-debug.log
/storage/oauth-private.key
/storage/oauth-public.key
logs/*
*.cache
.vagrant
*.log
*.retry
\.php_cs\.dist
phpmd\.xml
/public/storage
_ide_helper.php
.phpstorm.meta.php
_ide_helper_models.php
/.phplint-cache
storage/ldap_client_tls.cert
storage/ldap_client_tls.key
.DS_Store
/app/config/database.php
/app/config/mail.php
/app/database/*.sqlite
/app/storage/logs/*
/app/storage/meta/services.json
/app/storage/sessions/*
/app/storage/views/*
-19
View File
@@ -1,19 +0,0 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
# Make sure .env files not not browseable if in a sub-directory.
<FilesMatch "\.env$">
# Apache 2.2
<IfModule !authz_core_module>
Deny from all
</IfModule>
# Apache 2.4+
<IfModule authz_core_module>
Require all denied
</IfModule>
</FilesMatch>
</IfModule>
-1
View File
@@ -1 +0,0 @@
v18.16.0
-10
View File
@@ -1,10 +0,0 @@
{
"DOC1": "This file is meant to be pulled from the current HEAD of the desired branch, NOT referenced locally",
"DOC2": "In other words, what you see locally are the requirements for your _current_ install",
"DOC3": "Please don't rely on these versions for planning upgrades unless you've fetched the most recent version",
"DOC4": "You should really just ignore it and run upgrade.php. Really",
"php_min_version": "8.1.0",
"php_max_major_minor": "8.3",
"php_max_wontwork": "8.4.0",
"current_snipeit_version": "7.0"
}
-74
View File
@@ -1,74 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at abuse@snipeitapp.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
Regular → Executable
+2 -5
View File
@@ -1,6 +1,3 @@
### Contributing
# Contribution Guidelines
Please see the documentation on [contributing and developing for Snipe-IT](https://snipe-it.readme.io/docs/contributing-overview).
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
Please submit all issues and pull requests to the [snipe/snipe-it](http://github.com/snipe/snipe-it) repository in the develop branch!
-461
View File
@@ -1,461 +0,0 @@
Thanks goes to all of these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) who have helped Snipe-IT get this far:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.snipe.net"><img src="https://avatars3.githubusercontent.com/u/197404?v=3?s=110" width="110px;" alt="snipe"/><br /><sub><b>snipe</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Code">💻</a> <a href="#infra-snipe" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Documentation">📖</a> <a href="https://github.com/snipe/snipe-it/commits?author=snipe" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Asnipe" title="Bug reports">🐛</a> <a href="#design-snipe" title="Design">🎨</a> <a href="https://github.com/snipe/snipe-it/pulls?q=is%3Apr+reviewed-by%3Asnipe" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.uberbrady.com"><img src="https://avatars0.githubusercontent.com/u/36335?v=3?s=110" width="110px;" alt="Brady Wetherington"/><br /><sub><b>Brady Wetherington</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=uberbrady" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=uberbrady" title="Documentation">📖</a> <a href="#infra-uberbrady" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/snipe/snipe-it/pulls?q=is%3Apr+reviewed-by%3Auberbrady" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dmeltzer"><img src="https://avatars0.githubusercontent.com/u/3803132?v=3?s=110" width="110px;" alt="Daniel Meltzer"/><br /><sub><b>Daniel Meltzer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/commits?author=dmeltzer" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.tuckertechonline.com"><img src="https://avatars0.githubusercontent.com/u/1609106?v=3?s=110" width="110px;" alt="Michael T"/><br /><sub><b>Michael T</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mtucker6784" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/madd15"><img src="https://avatars2.githubusercontent.com/u/3274937?v=3?s=110" width="110px;" alt="madd15"/><br /><sub><b>madd15</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=madd15" title="Documentation">📖</a> <a href="#question-madd15" title="Answering Questions">💬</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vsposato"><img src="https://avatars2.githubusercontent.com/u/894126?v=3?s=110" width="110px;" alt="Vincent Sposato"/><br /><sub><b>Vincent Sposato</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vsposato" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vjandrea"><img src="https://avatars0.githubusercontent.com/u/1639757?v=3?s=110" width="110px;" alt="Andrea Bergamasco"/><br /><sub><b>Andrea Bergamasco</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vjandrea" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kpawelski"><img src="https://avatars0.githubusercontent.com/u/10640152?v=3?s=110" width="110px;" alt="Karol"/><br /><sub><b>Karol</b></sub></a><br /><a href="#translation-kpawelski" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=kpawelski" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://blog.morph027.de/"><img src="https://avatars3.githubusercontent.com/u/600106?v=3?s=110" width="110px;" alt="morph027"/><br /><sub><b>morph027</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=morph027" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fvleminckx"><img src="https://avatars3.githubusercontent.com/u/22935755?v=3?s=110" width="110px;" alt="fvleminckx"/><br /><sub><b>fvleminckx</b></sub></a><br /><a href="#infra-fvleminckx" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itsupportcmsukorg"><img src="https://avatars2.githubusercontent.com/u/15633547?v=3?s=110" width="110px;" alt="itsupportcmsukorg"/><br /><sub><b>itsupportcmsukorg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=itsupportcmsukorg" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Aitsupportcmsukorg" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://override.io"><img src="https://avatars3.githubusercontent.com/u/12373799?v=3?s=110" width="110px;" alt="Frank"/><br /><sub><b>Frank</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=base-zero" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ghost"><img src="https://avatars0.githubusercontent.com/u/10137?v=3?s=110" width="110px;" alt="Deleted user"/><br /><sub><b>Deleted user</b></sub></a><br /><a href="#translation-ghost" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=ghost" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tiagom62"><img src="https://avatars1.githubusercontent.com/u/10802313?v=3?s=110" width="110px;" alt="tiagom62"/><br /><sub><b>tiagom62</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tiagom62" title="Code">💻</a> <a href="#infra-tiagom62" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rystaf"><img src="https://avatars3.githubusercontent.com/u/2389047?v=3?s=110" width="110px;" alt="Ryan Stafford"/><br /><sub><b>Ryan Stafford</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rystaf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehanlon"><img src="https://avatars2.githubusercontent.com/u/10345935?v=3?s=110" width="110px;" alt="Eammon Hanlon"/><br /><sub><b>Eammon Hanlon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ehanlon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zjean"><img src="https://avatars0.githubusercontent.com/u/441924?v=3?s=110" width="110px;" alt="zjean"/><br /><sub><b>zjean</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zjean" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.frei.media"><img src="https://avatars0.githubusercontent.com/u/12660103?v=3?s=110" width="110px;" alt="Matthias Frei"/><br /><sub><b>Matthias Frei</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FREImedia" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/opsydev"><img src="https://avatars0.githubusercontent.com/u/3767518?v=3?s=110" width="110px;" alt="opsydev"/><br /><sub><b>opsydev</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=opsydev" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.ddreier.com"><img src="https://avatars1.githubusercontent.com/u/82290?v=3?s=110" width="110px;" alt="Daniel Dreier"/><br /><sub><b>Daniel Dreier</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ddreier" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://rassie.org"><img src="https://avatars0.githubusercontent.com/u/23448?v=3?s=110" width="110px;" alt="Nikolai Prokoschenko"/><br /><sub><b>Nikolai Prokoschenko</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rassie" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/YetAnotherCodeMonkey"><img src="https://avatars0.githubusercontent.com/u/13452757?v=3?s=110" width="110px;" alt="Drew"/><br /><sub><b>Drew</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=YetAnotherCodeMonkey" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/merid14"><img src="https://avatars0.githubusercontent.com/u/1342320?v=3?s=110" width="110px;" alt="Walter"/><br /><sub><b>Walter</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=merid14" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/balous"><img src="https://avatars3.githubusercontent.com/u/11254614?v=3?s=110" width="110px;" alt="Petr Baloun"/><br /><sub><b>Petr Baloun</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=balous" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reidblomquist"><img src="https://avatars0.githubusercontent.com/u/6117660?v=3?s=110" width="110px;" alt="reidblomquist"/><br /><sub><b>reidblomquist</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=reidblomquist" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mathieuk"><img src="https://avatars0.githubusercontent.com/u/539914?v=3?s=110" width="110px;" alt="Mathieu Kooiman"/><br /><sub><b>Mathieu Kooiman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mathieuk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/csayre"><img src="https://avatars3.githubusercontent.com/u/6606421?v=3?s=110" width="110px;" alt="csayre"/><br /><sub><b>csayre</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=csayre" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/adamdunson"><img src="https://avatars1.githubusercontent.com/u/768488?v=3?s=110" width="110px;" alt="Adam Dunson"/><br /><sub><b>Adam Dunson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adamdunson" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/thehereward"><img src="https://avatars0.githubusercontent.com/u/5547470?v=3?s=110" width="110px;" alt="Hereward"/><br /><sub><b>Hereward</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thehereward" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/swoopdk"><img src="https://avatars0.githubusercontent.com/u/5802977?v=3?s=110" width="110px;" alt="swoopdk"/><br /><sub><b>swoopdk</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=swoopdk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://linkedin.com/in/ahimta"><img src="https://avatars1.githubusercontent.com/u/3470403?v=3?s=110" width="110px;" alt="Abdullah Alansari"/><br /><sub><b>Abdullah Alansari</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Ahimta" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MicaelRodrigues"><img src="https://avatars0.githubusercontent.com/u/796443?v=3?s=110" width="110px;" alt="Micael Rodrigues"/><br /><sub><b>Micael Rodrigues</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=MicaelRodrigues" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://macadmincorner.com"><img src="https://avatars0.githubusercontent.com/u/614564?v=3?s=110" width="110px;" alt="Patrick Gallagher"/><br /><sub><b>Patrick Gallagher</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=patgmac" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Miliamber"><img src="https://avatars3.githubusercontent.com/u/7165922?v=3?s=110" width="110px;" alt="Miliamber"/><br /><sub><b>Miliamber</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Miliamber" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hawk554"><img src="https://avatars3.githubusercontent.com/u/861766?v=3?s=110" width="110px;" alt="hawk554"/><br /><sub><b>hawk554</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=hawk554" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://jbirdkerr.net"><img src="https://avatars1.githubusercontent.com/u/1695622?v=3?s=110" width="110px;" alt="Justin Kerr"/><br /><sub><b>Justin Kerr</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jbirdkerr" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.irasnyder.com/devel/"><img src="https://avatars3.githubusercontent.com/u/11426176?v=3?s=110" width="110px;" alt="Ira W. Snyder"/><br /><sub><b>Ira W. Snyder</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=irasnyd" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aalaily"><img src="https://avatars2.githubusercontent.com/u/2475759?v=3?s=110" width="110px;" alt="Aladin Alaily"/><br /><sub><b>Aladin Alaily</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=aalaily" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kobie-chasehansen"><img src="https://avatars0.githubusercontent.com/u/10247644?v=3?s=110" width="110px;" alt="Chase Hansen"/><br /><sub><b>Chase Hansen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kobie-chasehansen" title="Code">💻</a> <a href="#question-kobie-chasehansen" title="Answering Questions">💬</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3Akobie-chasehansen" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/IDM-Helpdesk"><img src="https://avatars2.githubusercontent.com/u/13545400?v=3?s=110" width="110px;" alt="IDM Helpdesk"/><br /><sub><b>IDM Helpdesk</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=IDM-Helpdesk" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://balticer.de"><img src="https://avatars2.githubusercontent.com/u/614439?v=3?s=110" width="110px;" alt="Kai"/><br /><sub><b>Kai</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=balticer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.michaeldaniels.me"><img src="https://avatars1.githubusercontent.com/u/8762511?v=3?s=110" width="110px;" alt="Michael Daniels"/><br /><sub><b>Michael Daniels</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mdaniels5757" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://tomcastleman.me"><img src="https://avatars3.githubusercontent.com/u/1532660?v=3?s=110" width="110px;" alt="Tom Castleman"/><br /><sub><b>Tom Castleman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tomcastleman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DanielNemanic"><img src="https://avatars3.githubusercontent.com/u/10723243?v=3?s=110" width="110px;" alt="Daniel Nemanic"/><br /><sub><b>Daniel Nemanic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DanielNemanic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/southwolf"><img src="https://avatars0.githubusercontent.com/u/150648?v=3?s=110" width="110px;" alt="SouthWolf"/><br /><sub><b>SouthWolf</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=southwolf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ivarne"><img src="https://avatars2.githubusercontent.com/u/131616?v=3?s=110" width="110px;" alt="Ivar Nesje"/><br /><sub><b>Ivar Nesje</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ivarne" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.j0k3r.net"><img src="https://avatars1.githubusercontent.com/u/62333?v=3?s=110" width="110px;" alt="Jérémy Benoist"/><br /><sub><b>Jérémy Benoist</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=j0k3r" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cleathley"><img src="https://avatars2.githubusercontent.com/u/724344?v=3?s=110" width="110px;" alt="Chris Leathley"/><br /><sub><b>Chris Leathley</b></sub></a><br /><a href="#infra-cleathley" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/splaer"><img src="https://avatars0.githubusercontent.com/u/972498?v=3?s=110" width="110px;" alt="splaer"/><br /><sub><b>splaer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/issues?q=author%3Asplaer" title="Bug reports">🐛</a> <a href="https://github.com/snipe/snipe-it/commits?author=splaer" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.joeferguson.me"><img src="https://avatars1.githubusercontent.com/u/967362?v=3?s=110" width="110px;" alt="Joe Ferguson"/><br /><sub><b>Joe Ferguson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=svpernova09" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/diwanicki"><img src="https://avatars3.githubusercontent.com/u/6108682?v=3?s=110" width="110px;" alt="diwanicki"/><br /><sub><b>diwanicki</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=diwanicki" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=diwanicki" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pakkua80"><img src="https://avatars3.githubusercontent.com/u/2527115?v=3?s=110" width="110px;" alt="Lee Thoong Ching"/><br /><sub><b>Lee Thoong Ching</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=pakkua80" title="Documentation">📖</a> <a href="https://github.com/snipe/snipe-it/commits?author=pakkua80" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://shu.io"><img src="https://avatars1.githubusercontent.com/u/461491?v=3?s=110" width="110px;" alt="Marek Šuppa"/><br /><sub><b>Marek Šuppa</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mrshu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mizar1616"><img src="https://avatars1.githubusercontent.com/u/8693762?v=3?s=110" width="110px;" alt="Juan J. Martinez"/><br /><sub><b>Juan J. Martinez</b></sub></a><br /><a href="#translation-mizar1616" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rrdial"><img src="https://avatars1.githubusercontent.com/u/1458388?v=3?s=110" width="110px;" alt="R Ryan Dial"/><br /><sub><b>R Ryan Dial</b></sub></a><br /><a href="#translation-rrdial" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/burlito"><img src="https://avatars2.githubusercontent.com/u/2871745?v=3?s=110" width="110px;" alt="Andrej Manduch"/><br /><sub><b>Andrej Manduch</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=burlito" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.cordeos.com"><img src="https://avatars0.githubusercontent.com/u/8341172?v=3?s=110" width="110px;" alt="Jay Richards"/><br /><sub><b>Jay Richards</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=technogenus" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://necurity.co.uk"><img src="https://avatars2.githubusercontent.com/u/7295127?v=3?s=110" width="110px;" alt="Alexander Innes"/><br /><sub><b>Alexander Innes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=leostat" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://buzzedword.codes"><img src="https://avatars2.githubusercontent.com/u/334485?v=3?s=110" width="110px;" alt="Danny Garcia"/><br /><sub><b>Danny Garcia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=buzzedword" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/archpoint"><img src="https://avatars2.githubusercontent.com/u/366855?v=3?s=110" width="110px;" alt="archpoint"/><br /><sub><b>archpoint</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=archpoint" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.jakemcgraw.com"><img src="https://avatars1.githubusercontent.com/u/67991?v=3?s=110" width="110px;" alt="Jake McGraw"/><br /><sub><b>Jake McGraw</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jakemcgraw" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FleischKarussel"><img src="https://avatars1.githubusercontent.com/u/1714374?v=3?s=110" width="110px;" alt="FleischKarussel"/><br /><sub><b>FleischKarussel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FleischKarussel" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/feeva"><img src="https://avatars3.githubusercontent.com/u/319644?v=3?s=110" width="110px;" alt="Dylan Yi"/><br /><sub><b>Dylan Yi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=feeva" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://FlashingCursor.com"><img src="https://avatars2.githubusercontent.com/u/857740?v=3?s=110" width="110px;" alt="Gil Rutkowski"/><br /><sub><b>Gil Rutkowski</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=flashingcursor" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.desmondmorris.com"><img src="https://avatars3.githubusercontent.com/u/129360?v=3?s=110" width="110px;" alt="Desmond Morris"/><br /><sub><b>Desmond Morris</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=desmondmorris" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://peelman.us"><img src="https://avatars2.githubusercontent.com/u/52936?v=3?s=110" width="110px;" alt="Nick Peelman"/><br /><sub><b>Nick Peelman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=peelman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://abrahamvegh.com"><img src="https://avatars0.githubusercontent.com/u/53161?v=3?s=110" width="110px;" alt="Abraham Vegh"/><br /><sub><b>Abraham Vegh</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=abrahamvegh" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rashivkp"><img src="https://avatars0.githubusercontent.com/u/2818680?v=3?s=110" width="110px;" alt="Mohamed Rashid"/><br /><sub><b>Mohamed Rashid</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rashivkp" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hinchk.github.io"><img src="https://avatars3.githubusercontent.com/u/1509456?v=3?s=110" width="110px;" alt="Kasey"/><br /><sub><b>Kasey</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=HinchK" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BrettFagerlund"><img src="https://avatars2.githubusercontent.com/u/10522541?v=3?s=110" width="110px;" alt="Brett"/><br /><sub><b>Brett</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BrettFagerlund" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://jasonspriggs.com"><img src="https://avatars2.githubusercontent.com/u/16108587?v=3?s=110" width="110px;" alt="Jason Spriggs"/><br /><sub><b>Jason Spriggs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jasonspriggs" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://n8felton.wordpress.com"><img src="https://avatars2.githubusercontent.com/u/1134568?v=3?s=110" width="110px;" alt="Nate Felton"/><br /><sub><b>Nate Felton</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=n8felton" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://homepages.dcc.ufmg.br/~manassesferreira"><img src="https://avatars2.githubusercontent.com/u/14036694?v=3?s=110" width="110px;" alt="Manasses Ferreira"/><br /><sub><b>Manasses Ferreira</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=manassesferreira" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/steveelwood"><img src="https://avatars0.githubusercontent.com/u/15913949?v=3?s=110" width="110px;" alt="Steve"/><br /><sub><b>Steve</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=steveelwood" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/matc"><img src="https://avatars1.githubusercontent.com/u/3361683?v=3?s=110" width="110px;" alt="matc"/><br /><sub><b>matc</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=matc" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.davisracingteam.com"><img src="https://avatars3.githubusercontent.com/u/7405702?v=3?s=110" width="110px;" alt="Cole R. Davis"/><br /><sub><b>Cole R. Davis</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=VanillaNinjaD" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gibsonjoshua55"><img src="https://avatars2.githubusercontent.com/u/10167681?v=3?s=110" width="110px;" alt="gibsonjoshua55"/><br /><sub><b>gibsonjoshua55</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gibsonjoshua55" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zwerch"><img src="https://avatars2.githubusercontent.com/u/2809241?v=4?s=110" width="110px;" alt="Robin Temme"/><br /><sub><b>Robin Temme</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zwerch" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/imanghafoori1"><img src="https://avatars0.githubusercontent.com/u/6961695?v=4?s=110" width="110px;" alt="Iman"/><br /><sub><b>Iman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=imanghafoori1" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/richardhofman6"><img src="https://avatars1.githubusercontent.com/u/6551003?v=4?s=110" width="110px;" alt="Richard Hofman"/><br /><sub><b>Richard Hofman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=richardhofman6" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gizzmojr"><img src="https://avatars0.githubusercontent.com/u/3697569?v=4?s=110" width="110px;" alt="gizzmojr"/><br /><sub><b>gizzmojr</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gizzmojr" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/imjennyli"><img src="https://avatars3.githubusercontent.com/u/404729?v=4?s=110" width="110px;" alt="Jenny Li"/><br /><sub><b>Jenny Li</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=imjennyli" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GeoffYoung"><img src="https://avatars0.githubusercontent.com/u/869227?v=4?s=110" width="110px;" alt="Geoff Young"/><br /><sub><b>Geoff Young</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=GeoffYoung" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.elliotblackburn.com"><img src="https://avatars3.githubusercontent.com/u/1068477?v=4?s=110" width="110px;" alt="Elliot Blackburn"/><br /><sub><b>Elliot Blackburn</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BlueHatbRit" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://andmemasin.eu"><img src="https://avatars1.githubusercontent.com/u/6357451?v=4?s=110" width="110px;" alt="Tõnis Ormisson"/><br /><sub><b>Tõnis Ormisson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TonisOrmisson" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.nicolai-essig.de"><img src="https://avatars0.githubusercontent.com/u/449411?v=4?s=110" width="110px;" alt="Nicolai Essig"/><br /><sub><b>Nicolai Essig</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thakilla" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/techincolor"><img src="https://avatars1.githubusercontent.com/u/14809698?v=4?s=110" width="110px;" alt="Danielle"/><br /><sub><b>Danielle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=techincolor" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TheVakman"><img src="https://avatars1.githubusercontent.com/u/18545156?v=4?s=110" width="110px;" alt="Lawrence"/><br /><sub><b>Lawrence</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TheVakman" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/issues?q=author%3ATheVakman" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/uknzaeinozpas"><img src="https://avatars1.githubusercontent.com/u/22473767?v=4?s=110" width="110px;" alt="uknzaeinozpas"/><br /><sub><b>uknzaeinozpas</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas" title="Tests">⚠️</a> <a href="https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Gelob"><img src="https://avatars3.githubusercontent.com/u/422752?v=4?s=110" width="110px;" alt="Ryan"/><br /><sub><b>Ryan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Gelob" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vcordes79"><img src="https://avatars1.githubusercontent.com/u/10672546?v=4?s=110" width="110px;" alt="vcordes79"/><br /><sub><b>vcordes79</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vcordes79" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fordster78"><img src="https://avatars3.githubusercontent.com/u/27958330?v=4?s=110" width="110px;" alt="fordster78"/><br /><sub><b>fordster78</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fordster78" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/CronKz"><img src="https://avatars0.githubusercontent.com/u/34064225?v=4?s=110" width="110px;" alt="CronKz"/><br /><sub><b>CronKz</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=CronKz" title="Code">💻</a> <a href="#translation-CronKz" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tdb"><img src="https://avatars1.githubusercontent.com/u/585486?v=4?s=110" width="110px;" alt="Tim Bishop"/><br /><sub><b>Tim Bishop</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tdb" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.seanmcilvenna.com"><img src="https://avatars2.githubusercontent.com/u/5384694?v=4?s=110" width="110px;" alt="Sean McIlvenna"/><br /><sub><b>Sean McIlvenna</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=seanmcilvenna" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cepacs"><img src="https://avatars3.githubusercontent.com/u/36515590?v=4?s=110" width="110px;" alt="cepacs"/><br /><sub><b>cepacs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/issues?q=author%3Acepacs" title="Bug reports">🐛</a> <a href="https://github.com/snipe/snipe-it/commits?author=cepacs" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lea-mink"><img src="https://avatars2.githubusercontent.com/u/37537300?v=4?s=110" width="110px;" alt="lea-mink"/><br /><sub><b>lea-mink</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lea-mink" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hannahtinkler"><img src="https://avatars0.githubusercontent.com/u/7140719?v=4?s=110" width="110px;" alt="Hannah Tinkler"/><br /><sub><b>Hannah Tinkler</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=hannahtinkler" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/doekman"><img src="https://avatars1.githubusercontent.com/u/1086388?v=4?s=110" width="110px;" alt="Doeke Zanstra"/><br /><sub><b>Doeke Zanstra</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=doekman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.sdhd.nl/"><img src="https://avatars1.githubusercontent.com/u/4325936?v=4?s=110" width="110px;" alt="Djamon Staal"/><br /><sub><b>Djamon Staal</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=SjamonDaal" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EarlRamirez"><img src="https://avatars3.githubusercontent.com/u/12306859?v=4?s=110" width="110px;" alt="Earl Ramirez"/><br /><sub><b>Earl Ramirez</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=EarlRamirez" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/RichardRay"><img src="https://avatars2.githubusercontent.com/u/8671456?v=4?s=110" width="110px;" alt="Richard Ray Thomas"/><br /><sub><b>Richard Ray Thomas</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=RichardRay" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.taisun.io/"><img src="https://avatars3.githubusercontent.com/u/1852688?v=4?s=110" width="110px;" alt="Ryan Kuba"/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=thelamer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ParadoxGuitarist"><img src="https://avatars1.githubusercontent.com/u/6751928?v=4?s=110" width="110px;" alt="Brian Monroe"/><br /><sub><b>Brian Monroe</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ParadoxGuitarist" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/plexorama"><img src="https://avatars1.githubusercontent.com/u/605167?v=4?s=110" width="110px;" alt="plexorama"/><br /><sub><b>plexorama</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=plexorama" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://tilldeeke.de"><img src="https://avatars2.githubusercontent.com/u/1795149?v=4?s=110" width="110px;" alt="Till Deeke"/><br /><sub><b>Till Deeke</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tilldeeke" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/5quirrel"><img src="https://avatars0.githubusercontent.com/u/12634129?v=4?s=110" width="110px;" alt="5quirrel"/><br /><sub><b>5quirrel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=5quirrel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jasonlshelton"><img src="https://avatars1.githubusercontent.com/u/13071957?v=4?s=110" width="110px;" alt="Jason"/><br /><sub><b>Jason</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jasonlshelton" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chemfy"><img src="https://avatars3.githubusercontent.com/u/7128321?v=4?s=110" width="110px;" alt="Antti"/><br /><sub><b>Antti</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chemfy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DeusMaximus"><img src="https://avatars3.githubusercontent.com/u/10080364?v=4?s=110" width="110px;" alt="DeusMaximus"/><br /><sub><b>DeusMaximus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=DeusMaximus" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/A-ROYAL"><img src="https://avatars2.githubusercontent.com/u/16384611?v=4?s=110" width="110px;" alt="a-royal"/><br /><sub><b>a-royal</b></sub></a><br /><a href="#translation-A-ROYAL" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/albertoaldrigo"><img src="https://avatars0.githubusercontent.com/u/5358208?v=4?s=110" width="110px;" alt="Alberto Aldrigo"/><br /><sub><b>Alberto Aldrigo</b></sub></a><br /><a href="#translation-albertoaldrigo" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://alex.stanev.org/blog"><img src="https://avatars0.githubusercontent.com/u/1412342?v=4?s=110" width="110px;" alt="Alex Stanev"/><br /><sub><b>Alex Stanev</b></sub></a><br /><a href="#translation-RealEnder" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://devel.itsolution2.de"><img src="https://avatars0.githubusercontent.com/u/177295?v=4?s=110" width="110px;" alt="Andreas Rehm"/><br /><sub><b>Andreas Rehm</b></sub></a><br /><a href="#translation-sirrus" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xelan"><img src="https://avatars0.githubusercontent.com/u/5080535?v=4?s=110" width="110px;" alt="Andreas Erhard"/><br /><sub><b>Andreas Erhard</b></sub></a><br /><a href="#translation-xelan" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/angeldeejay"><img src="https://avatars2.githubusercontent.com/u/142350?v=4?s=110" width="110px;" alt="Andrés Vanegas Jiménez"/><br /><sub><b>Andrés Vanegas Jiménez</b></sub></a><br /><a href="#translation-angeldeejay" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aschiavon91"><img src="https://avatars0.githubusercontent.com/u/3910403?v=4?s=110" width="110px;" alt="Antonio Schiavon"/><br /><sub><b>Antonio Schiavon</b></sub></a><br /><a href="#translation-aschiavon91" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benunter"><img src="https://avatars0.githubusercontent.com/u/10464547?v=4?s=110" width="110px;" alt="benunter"/><br /><sub><b>benunter</b></sub></a><br /><a href="#translation-benunter" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://catweb24.pl"><img src="https://avatars1.githubusercontent.com/u/5038647?v=4?s=110" width="110px;" alt="Borys Żmuda"/><br /><sub><b>Borys Żmuda</b></sub></a><br /><a href="#translation-rudashi" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chibacityblues"><img src="https://avatars0.githubusercontent.com/u/5539359?v=4?s=110" width="110px;" alt="chibacityblues"/><br /><sub><b>chibacityblues</b></sub></a><br /><a href="#translation-chibacityblues" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cwlin0416"><img src="https://avatars1.githubusercontent.com/u/1954830?v=4?s=110" width="110px;" alt="Chien Wei Lin"/><br /><sub><b>Chien Wei Lin</b></sub></a><br /><a href="#translation-cwlin0416" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Againstreality"><img src="https://avatars3.githubusercontent.com/u/11700533?v=4?s=110" width="110px;" alt="Christian Schuster"/><br /><sub><b>Christian Schuster</b></sub></a><br /><a href="#translation-Againstreality" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://chriss.webhostid.com"><img src="https://avatars1.githubusercontent.com/u/4308704?v=4?s=110" width="110px;" alt="Christian Stefanus"/><br /><sub><b>Christian Stefanus</b></sub></a><br /><a href="#translation-kopi-item" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://wxcafe.net"><img src="https://avatars3.githubusercontent.com/u/3009327?v=4?s=110" width="110px;" alt="wxcafé"/><br /><sub><b>wxcafé</b></sub></a><br /><a href="#translation-wxcafe" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dpyroc"><img src="https://avatars3.githubusercontent.com/u/35761525?v=4?s=110" width="110px;" alt="dpyroc"/><br /><sub><b>dpyroc</b></sub></a><br /><a href="#translation-dpyroc" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.friedlmaier.net"><img src="https://avatars1.githubusercontent.com/u/2153639?v=4?s=110" width="110px;" alt="Daniel Friedlmaier"/><br /><sub><b>Daniel Friedlmaier</b></sub></a><br /><a href="#translation-da-friedl" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/danielheene"><img src="https://avatars1.githubusercontent.com/u/2947640?v=4?s=110" width="110px;" alt="Daniel Heene"/><br /><sub><b>Daniel Heene</b></sub></a><br /><a href="#translation-danielheene" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/danielcb"><img src="https://avatars3.githubusercontent.com/u/319022?v=4?s=110" width="110px;" alt="danielcb"/><br /><sub><b>danielcb</b></sub></a><br /><a href="#translation-danielcb" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dominiksenti"><img src="https://avatars3.githubusercontent.com/u/15846537?v=4?s=110" width="110px;" alt="Dominik Senti"/><br /><sub><b>Dominik Senti</b></sub></a><br /><a href="#translation-dominiksenti" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.konectik.com"><img src="https://avatars0.githubusercontent.com/u/25570954?v=4?s=110" width="110px;" alt="Eric Gautheron"/><br /><sub><b>Eric Gautheron</b></sub></a><br /><a href="#translation-EpixFr" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://erlpil.com"><img src="https://avatars1.githubusercontent.com/u/5732623?v=4?s=110" width="110px;" alt="Erlend Pilø"/><br /><sub><b>Erlend Pilø</b></sub></a><br /><a href="#translation-Erlpil" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://fabio.technology"><img src="https://avatars0.githubusercontent.com/u/541832?v=4?s=110" width="110px;" alt="Fabio Rapposelli"/><br /><sub><b>Fabio Rapposelli</b></sub></a><br /><a href="#translation-frapposelli" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fgbs"><img src="https://avatars2.githubusercontent.com/u/3605240?v=4?s=110" width="110px;" alt="Felipe Barros"/><br /><sub><b>Felipe Barros</b></sub></a><br /><a href="#translation-fgbs" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/possebon"><img src="https://avatars0.githubusercontent.com/u/257745?v=4?s=110" width="110px;" alt="Fernando Possebon"/><br /><sub><b>Fernando Possebon</b></sub></a><br /><a href="#translation-possebon" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gdraque"><img src="https://avatars3.githubusercontent.com/u/2540832?v=4?s=110" width="110px;" alt="gdraque"/><br /><sub><b>gdraque</b></sub></a><br /><a href="#translation-gdraque" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/georgwallisch"><img src="https://avatars0.githubusercontent.com/u/23440381?v=4?s=110" width="110px;" alt="Georg Wallisch"/><br /><sub><b>Georg Wallisch</b></sub></a><br /><a href="#translation-georgwallisch" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jgroblesr85"><img src="https://avatars1.githubusercontent.com/u/9852832?v=4?s=110" width="110px;" alt="Gerardo Robles"/><br /><sub><b>Gerardo Robles</b></sub></a><br /><a href="#translation-jgroblesr85" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://t.me/Gluek"><img src="https://avatars2.githubusercontent.com/u/11082640?v=4?s=110" width="110px;" alt="Gluek"/><br /><sub><b>Gluek</b></sub></a><br /><a href="#translation-mrgluek" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AdnanAbuShahad"><img src="https://avatars0.githubusercontent.com/u/6847946?v=4?s=110" width="110px;" alt="AdnanAbuShahad"/><br /><sub><b>AdnanAbuShahad</b></sub></a><br /><a href="#translation-AdnanAbuShahad" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://hafidzi.my"><img src="https://avatars1.githubusercontent.com/u/3580608?v=4?s=110" width="110px;" alt="Hafidzi My"/><br /><sub><b>Hafidzi My</b></sub></a><br /><a href="#translation-hafidzi" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fofwisdom"><img src="https://avatars2.githubusercontent.com/u/205521?v=4?s=110" width="110px;" alt="Harim Park"/><br /><sub><b>Harim Park</b></sub></a><br /><a href="#translation-fofwisdom" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.kentsson.se"><img src="https://avatars2.githubusercontent.com/u/3333841?v=4?s=110" width="110px;" alt="Henrik Kentsson"/><br /><sub><b>Henrik Kentsson</b></sub></a><br /><a href="#translation-Kentsson" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/husnulyaqien"><img src="https://avatars0.githubusercontent.com/u/36551034?v=4?s=110" width="110px;" alt="Husnul Yaqien"/><br /><sub><b>Husnul Yaqien</b></sub></a><br /><a href="#translation-husnulyaqien" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://abaalkhail.org"><img src="https://avatars1.githubusercontent.com/u/2372747?v=4?s=110" width="110px;" alt="Ibrahim"/><br /><sub><b>Ibrahim</b></sub></a><br /><a href="#translation-abaalkh" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/igolman"><img src="https://avatars0.githubusercontent.com/u/1389334?v=4?s=110" width="110px;" alt="igolman"/><br /><sub><b>igolman</b></sub></a><br /><a href="#translation-igolman" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itangiang"><img src="https://avatars1.githubusercontent.com/u/3257070?v=4?s=110" width="110px;" alt="itangiang"/><br /><sub><b>itangiang</b></sub></a><br /><a href="#translation-itangiang" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jarby1211"><img src="https://avatars2.githubusercontent.com/u/14814254?v=4?s=110" width="110px;" alt="jarby1211"/><br /><sub><b>jarby1211</b></sub></a><br /><a href="#translation-jarby1211" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://jwillker.com"><img src="https://avatars3.githubusercontent.com/u/6719357?v=4?s=110" width="110px;" alt="Jhonn Willker"/><br /><sub><b>Jhonn Willker</b></sub></a><br /><a href="#translation-JohnWillker" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/joxelito94"><img src="https://avatars2.githubusercontent.com/u/10983635?v=4?s=110" width="110px;" alt="Jose"/><br /><sub><b>Jose</b></sub></a><br /><a href="#translation-joxelito94" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/laopangzi"><img src="https://avatars0.githubusercontent.com/u/5206122?v=4?s=110" width="110px;" alt="laopangzi"/><br /><sub><b>laopangzi</b></sub></a><br /><a href="#translation-laopangzi" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://usrportage.de"><img src="https://avatars2.githubusercontent.com/u/79707?v=4?s=110" width="110px;" alt="Lars Strojny"/><br /><sub><b>Lars Strojny</b></sub></a><br /><a href="#translation-lstrojny" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/marcosbl"><img src="https://avatars0.githubusercontent.com/u/389801?v=4?s=110" width="110px;" alt="MarcosBL"/><br /><sub><b>MarcosBL</b></sub></a><br /><a href="#translation-MarcosBL" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mariejoyacajes"><img src="https://avatars3.githubusercontent.com/u/35664606?v=4?s=110" width="110px;" alt="marie joy cajes"/><br /><sub><b>marie joy cajes</b></sub></a><br /><a href="#translation-mariejoyacajes" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.markjohansen.dk"><img src="https://avatars2.githubusercontent.com/u/3052816?v=4?s=110" width="110px;" alt="Mark S. Johansen"/><br /><sub><b>Mark S. Johansen</b></sub></a><br /><a href="#translation-msjohansen" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://martinstub.dk"><img src="https://avatars2.githubusercontent.com/u/982885?v=4?s=110" width="110px;" alt="Martin Stub"/><br /><sub><b>Martin Stub</b></sub></a><br /><a href="#translation-stubben" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/meyerf99"><img src="https://avatars2.githubusercontent.com/u/28959963?v=4?s=110" width="110px;" alt="Meyer Flavio"/><br /><sub><b>Meyer Flavio</b></sub></a><br /><a href="#translation-meyerf99" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MicaelRodrigues"><img src="https://avatars3.githubusercontent.com/u/796443?v=4?s=110" width="110px;" alt="Micael Rodrigues"/><br /><sub><b>Micael Rodrigues</b></sub></a><br /><a href="#translation-MicaelRodrigues" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://rubixy.com/"><img src="https://avatars0.githubusercontent.com/u/10481331?v=4?s=110" width="110px;" alt="Mikael Rasmussen"/><br /><sub><b>Mikael Rasmussen</b></sub></a><br /><a href="#translation-mikaelssen" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/IxFail"><img src="https://avatars1.githubusercontent.com/u/1544552?v=4?s=110" width="110px;" alt="IxFail"/><br /><sub><b>IxFail</b></sub></a><br /><a href="#translation-IxFail" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.mohammedfota.com"><img src="https://avatars3.githubusercontent.com/u/18483118?v=4?s=110" width="110px;" alt="Mohammed Fota"/><br /><sub><b>Mohammed Fota</b></sub></a><br /><a href="#translation-MohammedFota" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/omego"><img src="https://avatars0.githubusercontent.com/u/227080?v=4?s=110" width="110px;" alt="Moayad Alserihi"/><br /><sub><b>Moayad Alserihi</b></sub></a><br /><a href="#translation-omego" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/saymd"><img src="https://avatars0.githubusercontent.com/u/1680266?v=4?s=110" width="110px;" alt="saymd"/><br /><sub><b>saymd</b></sub></a><br /><a href="#translation-saymd" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nordsken.se"><img src="https://avatars0.githubusercontent.com/u/1826808?v=4?s=110" width="110px;" alt="Patrik Larsson"/><br /><sub><b>Patrik Larsson</b></sub></a><br /><a href="#translation-pooot" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/drcryo"><img src="https://avatars1.githubusercontent.com/u/20584746?v=4?s=110" width="110px;" alt="drcryo"/><br /><sub><b>drcryo</b></sub></a><br /><a href="#translation-drcryo" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pawel1615"><img src="https://avatars1.githubusercontent.com/u/19408004?v=4?s=110" width="110px;" alt="pawel1615"/><br /><sub><b>pawel1615</b></sub></a><br /><a href="#translation-pawel1615" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bodrovics"><img src="https://avatars2.githubusercontent.com/u/23340468?v=4?s=110" width="110px;" alt="bodrovics"/><br /><sub><b>bodrovics</b></sub></a><br /><a href="#translation-bodrovics" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/priatna"><img src="https://avatars0.githubusercontent.com/u/3257654?v=4?s=110" width="110px;" alt="priatna"/><br /><sub><b>priatna</b></sub></a><br /><a href="#translation-priatna" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://amayume.net"><img src="https://avatars1.githubusercontent.com/u/5358374?v=4?s=110" width="110px;" alt="Fan Jiang"/><br /><sub><b>Fan Jiang</b></sub></a><br /><a href="#translation-ProfFan" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ragnarcx"><img src="https://avatars1.githubusercontent.com/u/22555451?v=4?s=110" width="110px;" alt="ragnarcx"/><br /><sub><b>ragnarcx</b></sub></a><br /><a href="#translation-ragnarcx" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.reinvanhaaren.nl/"><img src="https://avatars2.githubusercontent.com/u/18654582?v=4?s=110" width="110px;" alt="Rein van Haaren"/><br /><sub><b>Rein van Haaren</b></sub></a><br /><a href="#translation-reinvanhaaren" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://dheche.songolimo.net"><img src="https://avatars1.githubusercontent.com/u/386672?v=4?s=110" width="110px;" alt="Teguh Dwicaksana"/><br /><sub><b>Teguh Dwicaksana</b></sub></a><br /><a href="#translation-dheche" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FRaccie"><img src="https://avatars2.githubusercontent.com/u/2572552?v=4?s=110" width="110px;" alt="fraccie"/><br /><sub><b>fraccie</b></sub></a><br /><a href="#translation-FRaccie" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vinzruzell"><img src="https://avatars0.githubusercontent.com/u/35182720?v=4?s=110" width="110px;" alt="vinzruzell"/><br /><sub><b>vinzruzell</b></sub></a><br /><a href="#translation-vinzruzell" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://kevinaustin.com"><img src="https://avatars1.githubusercontent.com/u/7883603?v=4?s=110" width="110px;" alt="Kevin Austin"/><br /><sub><b>Kevin Austin</b></sub></a><br /><a href="#translation-vipsystem" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://azuraweb.xyz"><img src="https://avatars3.githubusercontent.com/u/3861828?v=4?s=110" width="110px;" alt="Wira Sandy"/><br /><sub><b>Wira Sandy</b></sub></a><br /><a href="#translation-wira-sandy" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/GrayHoax"><img src="https://avatars2.githubusercontent.com/u/8663789?v=4?s=110" width="110px;" alt="Илья"/><br /><sub><b>Илья</b></sub></a><br /><a href="#translation-GrayHoax" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/godusevpn"><img src="https://avatars3.githubusercontent.com/u/30119111?v=4?s=110" width="110px;" alt="GodUseVPN"/><br /><sub><b>GodUseVPN</b></sub></a><br /><a href="#translation-godusevpn" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EngrZhou"><img src="https://avatars1.githubusercontent.com/u/745576?v=4?s=110" width="110px;" alt="周周"/><br /><sub><b>周周</b></sub></a><br /><a href="#translation-EngrZhou" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/takuy"><img src="https://avatars3.githubusercontent.com/u/1631095?v=4?s=110" width="110px;" alt="Sam"/><br /><sub><b>Sam</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=takuy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.illisian.com.au"><img src="https://avatars1.githubusercontent.com/u/264022?v=4?s=110" width="110px;" alt="Azerothian"/><br /><sub><b>Azerothian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Azerothian" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://macfoo.wordpress.com/"><img src="https://avatars1.githubusercontent.com/u/4930051?v=4?s=110" width="110px;" alt="Wes Hulette"/><br /><sub><b>Wes Hulette</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jwhulette" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/patrict"><img src="https://avatars0.githubusercontent.com/u/8134591?v=4?s=110" width="110px;" alt="patrict"/><br /><sub><b>patrict</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=patrict" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/VELIKII-DIVAN"><img src="https://avatars3.githubusercontent.com/u/2611616?v=4?s=110" width="110px;" alt="Dmitriy Minaev"/><br /><sub><b>Dmitriy Minaev</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=VELIKII-DIVAN" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liquidhorse"><img src="https://avatars0.githubusercontent.com/u/5132245?v=4?s=110" width="110px;" alt="liquidhorse"/><br /><sub><b>liquidhorse</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=liquidhorse" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://seld.be/"><img src="https://avatars1.githubusercontent.com/u/183678?v=4?s=110" width="110px;" alt="Jordi Boggiano"/><br /><sub><b>Jordi Boggiano</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Seldaek" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/inietov"><img src="https://avatars0.githubusercontent.com/u/653557?v=4?s=110" width="110px;" alt="Ivan Nieto"/><br /><sub><b>Ivan Nieto</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=inietov" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benrubson"><img src="https://avatars2.githubusercontent.com/u/6764151?v=4?s=110" width="110px;" alt="Ben RUBSON"/><br /><sub><b>Ben RUBSON</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=benrubson" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NMathar"><img src="https://avatars2.githubusercontent.com/u/8554558?v=4?s=110" width="110px;" alt="NMathar"/><br /><sub><b>NMathar</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=NMathar" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/smb"><img src="https://avatars1.githubusercontent.com/u/139566?v=4?s=110" width="110px;" alt="Steffen"/><br /><sub><b>Steffen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=smb" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Sxderp"><img src="https://avatars0.githubusercontent.com/u/6609453?v=4?s=110" width="110px;" alt="Sxderp"/><br /><sub><b>Sxderp</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Sxderp" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fanta8897"><img src="https://avatars1.githubusercontent.com/u/4807843?v=4?s=110" width="110px;" alt="fanta8897"/><br /><sub><b>fanta8897</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fanta8897" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://andreybolonin.com/phpconsulting/"><img src="https://avatars2.githubusercontent.com/u/2576509?v=4?s=110" width="110px;" alt="Andrey Bolonin"/><br /><sub><b>Andrey Bolonin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andreybolonin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.shinayoshi.net/"><img src="https://avatars3.githubusercontent.com/u/2173307?v=4?s=110" width="110px;" alt="shinayoshi"/><br /><sub><b>shinayoshi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=shinayoshi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reuser"><img src="https://avatars3.githubusercontent.com/u/2130159?v=4?s=110" width="110px;" alt="Hubert"/><br /><sub><b>Hubert</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=reuser" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://brashear.me"><img src="https://avatars0.githubusercontent.com/u/6865789?v=4?s=110" width="110px;" alt="KeenRivals"/><br /><sub><b>KeenRivals</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=KeenRivals" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/omyno"><img src="https://avatars3.githubusercontent.com/u/2902513?v=4?s=110" width="110px;" alt="omyno"/><br /><sub><b>omyno</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=omyno" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jackka"><img src="https://avatars1.githubusercontent.com/u/6271335?v=4?s=110" width="110px;" alt="Evgeny"/><br /><sub><b>Evgeny</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jackka" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://digitalist.se"><img src="https://avatars2.githubusercontent.com/u/1169963?v=4?s=110" width="110px;" alt="Colin Campbell"/><br /><sub><b>Colin Campbell</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=colin-campbell" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lubo"><img src="https://avatars3.githubusercontent.com/u/2872098?v=4?s=110" width="110px;" alt="Ľubomír Kučera"/><br /><sub><b>Ľubomír Kučera</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lubo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.sourceguru.net"><img src="https://avatars3.githubusercontent.com/u/570639?v=4?s=110" width="110px;" alt="Martin Meredith"/><br /><sub><b>Martin Meredith</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Mezzle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/timothyfarmer"><img src="https://avatars1.githubusercontent.com/u/7632599?v=4?s=110" width="110px;" alt="Tim Farmer"/><br /><sub><b>Tim Farmer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=timothyfarmer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mskrip"><img src="https://avatars0.githubusercontent.com/u/17459600?v=4?s=110" width="110px;" alt="Marián Skrip"/><br /><sub><b>Marián Skrip</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mskrip" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Godmartinz"><img src="https://avatars2.githubusercontent.com/u/47435081?v=4?s=110" width="110px;" alt="Godfrey Martinez"/><br /><sub><b>Godfrey Martinez</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Godmartinz" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bigtreeEdo"><img src="https://avatars1.githubusercontent.com/u/2075128?v=4?s=110" width="110px;" alt="bigtreeEdo"/><br /><sub><b>bigtreeEdo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bigtreeEdo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://colinmcneil.me/"><img src="https://avatars0.githubusercontent.com/u/5000430?v=4?s=110" width="110px;" alt="Colin McNeil"/><br /><sub><b>Colin McNeil</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ColinMcNeil" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JoKneeMo"><img src="https://avatars0.githubusercontent.com/u/421625?v=4?s=110" width="110px;" alt="JoKneeMo"/><br /><sub><b>JoKneeMo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JoKneeMo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.redbridge.se"><img src="https://avatars0.githubusercontent.com/u/54849013?v=4?s=110" width="110px;" alt="Joshi"/><br /><sub><b>Joshi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=joshi-redbridge" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/anthonypburns"><img src="https://avatars2.githubusercontent.com/u/15731458?v=4?s=110" width="110px;" alt="Anthony Burns"/><br /><sub><b>Anthony Burns</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=anthonypburns" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/johnson-yi"><img src="https://avatars1.githubusercontent.com/u/63399474?v=4?s=110" width="110px;" alt="johnson-yi"/><br /><sub><b>johnson-yi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=johnson-yi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://tangentmc.net"><img src="https://avatars1.githubusercontent.com/u/1862720?v=4?s=110" width="110px;" alt="Sanjay Govind"/><br /><sub><b>Sanjay Govind</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sanjay900" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://peter.upfold.org.uk/"><img src="https://avatars0.githubusercontent.com/u/1255375?v=4?s=110" width="110px;" alt="Peter Upfold"/><br /><sub><b>Peter Upfold</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PeterUpfold" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jbiel"><img src="https://avatars2.githubusercontent.com/u/961717?v=4?s=110" width="110px;" alt="Jared Biel"/><br /><sub><b>Jared Biel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jbiel" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dampfklon"><img src="https://avatars1.githubusercontent.com/u/1733625?v=4?s=110" width="110px;" alt="Dampfklon"/><br /><sub><b>Dampfklon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dampfklon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://communityclosing.com"><img src="https://avatars2.githubusercontent.com/u/52973156?v=4?s=110" width="110px;" alt="Charles Hamilton"/><br /><sub><b>Charles Hamilton</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chamilton-ccn" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/giannello"><img src="https://avatars.githubusercontent.com/u/551789?v=4?s=110" width="110px;" alt="Giuseppe Iannello"/><br /><sub><b>Giuseppe Iannello</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=giannello" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.peterdavehello.org/"><img src="https://avatars.githubusercontent.com/u/3691490?v=4?s=110" width="110px;" alt="Peter Dave Hello"/><br /><sub><b>Peter Dave Hello</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PeterDaveHello" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sigmoidal"><img src="https://avatars.githubusercontent.com/u/6106332?v=4?s=110" width="110px;" alt="sigmoidal"/><br /><sub><b>sigmoidal</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sigmoidal" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/phenixdotnet"><img src="https://avatars.githubusercontent.com/u/2082554?v=4?s=110" width="110px;" alt="Vincent Lainé"/><br /><sub><b>Vincent Lainé</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=phenixdotnet" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.lucas-pless.com"><img src="https://avatars.githubusercontent.com/u/1943040?v=4?s=110" width="110px;" alt="Lucas Pleß"/><br /><sub><b>Lucas Pleß</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=derlucas" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://twitter.com/iansltx"><img src="https://avatars.githubusercontent.com/u/472804?v=4?s=110" width="110px;" alt="Ian Littman"/><br /><sub><b>Ian Littman</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=iansltx" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PauloLuna"><img src="https://avatars.githubusercontent.com/u/3519029?v=4?s=110" width="110px;" alt="João Paulo"/><br /><sub><b>João Paulo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PauloLuna" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ThoBur"><img src="https://avatars.githubusercontent.com/u/70443365?v=4?s=110" width="110px;" alt="ThoBur"/><br /><sub><b>ThoBur</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ThoBur" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://phpprofi.ru/"><img src="https://avatars.githubusercontent.com/u/1972329?v=4?s=110" width="110px;" alt="Alexander Chibrikin"/><br /><sub><b>Alexander Chibrikin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=alek13" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/winstan"><img src="https://avatars.githubusercontent.com/u/438332?v=4?s=110" width="110px;" alt="Anthony Winstanley"/><br /><sub><b>Anthony Winstanley</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=winstan" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fashberg"><img src="https://avatars.githubusercontent.com/u/3075214?v=4?s=110" width="110px;" alt="Folke"/><br /><sub><b>Folke</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fashberg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/benwa"><img src="https://avatars.githubusercontent.com/u/1351571?v=4?s=110" width="110px;" alt="Bennett Blodinger"/><br /><sub><b>Bennett Blodinger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=benwa" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nmc.dev"><img src="https://avatars.githubusercontent.com/u/2974631?v=4?s=110" width="110px;" alt="NMC"/><br /><sub><b>NMC</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ncareau" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andres-baller"><img src="https://avatars.githubusercontent.com/u/52182449?v=4?s=110" width="110px;" alt="andres-baller"/><br /><sub><b>andres-baller</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andres-baller" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sean-borg"><img src="https://avatars.githubusercontent.com/u/67109348?v=4?s=110" width="110px;" alt="sean-borg"/><br /><sub><b>sean-borg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sean-borg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EDVLeer"><img src="https://avatars.githubusercontent.com/u/32170051?v=4?s=110" width="110px;" alt="EDVLeer"/><br /><sub><b>EDVLeer</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=EDVLeer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Kurokat"><img src="https://avatars.githubusercontent.com/u/23075196?v=4?s=110" width="110px;" alt="Kurokat"/><br /><sub><b>Kurokat</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Kurokat" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.kevinkoellmann.de"><img src="https://avatars.githubusercontent.com/u/915514?v=4?s=110" width="110px;" alt="Kevin Köllmann"/><br /><sub><b>Kevin Köllmann</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=koelle25" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sw-mreyes"><img src="https://avatars.githubusercontent.com/u/49025941?v=4?s=110" width="110px;" alt="sw-mreyes"/><br /><sub><b>sw-mreyes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sw-mreyes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://pittet.ca"><img src="https://avatars.githubusercontent.com/u/70129?v=4?s=110" width="110px;" alt="Joel Pittet"/><br /><sub><b>Joel Pittet</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=joelpittet" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://elyscape.com"><img src="https://avatars.githubusercontent.com/u/792695?v=4?s=110" width="110px;" alt="Eli Young"/><br /><sub><b>Eli Young</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=elyscape" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/raelldottin"><img src="https://avatars.githubusercontent.com/u/317015?v=4?s=110" width="110px;" alt="Raell Dottin"/><br /><sub><b>Raell Dottin</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=raelldottin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/misilot"><img src="https://avatars.githubusercontent.com/u/1446856?v=4?s=110" width="110px;" alt="Tom Misilo"/><br /><sub><b>Tom Misilo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=misilot" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://david.davenne.be"><img src="https://avatars.githubusercontent.com/u/4496300?v=4?s=110" width="110px;" alt="David Davenne"/><br /><sub><b>David Davenne</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JuustoMestari" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://markstenglein.com"><img src="https://avatars.githubusercontent.com/u/9255772?v=4?s=110" width="110px;" alt="Mark Stenglein"/><br /><sub><b>Mark Stenglein</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ocelotsloth" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ajsy"><img src="https://avatars.githubusercontent.com/u/35658596?v=4?s=110" width="110px;" alt="ajsy"/><br /><sub><b>ajsy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ajsy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/t3easy"><img src="https://avatars.githubusercontent.com/u/3628035?v=4?s=110" width="110px;" alt="Jan Kiesewetter"/><br /><sub><b>Jan Kiesewetter</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=t3easy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tetrachloromethane250"><img src="https://avatars.githubusercontent.com/u/79449630?v=4?s=110" width="110px;" alt="Tetrachloromethane250"/><br /><sub><b>Tetrachloromethane250</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Tetrachloromethane250" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.kajes.se/"><img src="https://avatars.githubusercontent.com/u/22004482?v=4?s=110" width="110px;" alt="Lars Kajes"/><br /><sub><b>Lars Kajes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kajes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Joly0"><img src="https://avatars.githubusercontent.com/u/13993216?v=4?s=110" width="110px;" alt="Joly0"/><br /><sub><b>Joly0</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Joly0" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/limeless"><img src="https://avatars.githubusercontent.com/u/1501022?v=4?s=110" width="110px;" alt="theburger"/><br /><sub><b>theburger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=limeless" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/deivishome"><img src="https://avatars.githubusercontent.com/u/36065681?v=4?s=110" width="110px;" alt="David Valin Alonso"/><br /><sub><b>David Valin Alonso</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=deivishome" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/andreaci"><img src="https://avatars.githubusercontent.com/u/8290389?v=4?s=110" width="110px;" alt="andreaci"/><br /><sub><b>andreaci</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=andreaci" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.jellesebreghts.be"><img src="https://avatars.githubusercontent.com/u/1828542?v=4?s=110" width="110px;" alt="Jelle Sebreghts"/><br /><sub><b>Jelle Sebreghts</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Jelle-S" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Skywalker-11"><img src="https://avatars.githubusercontent.com/u/11180862?v=4?s=110" width="110px;" alt="Michael Pietsch"/><br /><sub><b>Michael Pietsch</b></sub></a><br /></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sh1hab"><img src="https://avatars.githubusercontent.com/u/22068886?v=4?s=110" width="110px;" alt="Masudul Haque Shihab"/><br /><sub><b>Masudul Haque Shihab</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sh1hab" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.freedomdive.com/"><img src="https://avatars.githubusercontent.com/u/16099942?v=4?s=110" width="110px;" alt="Supapong Areeprasertkul"/><br /><sub><b>Supapong Areeprasertkul</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zybersup" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/psarossy"><img src="https://avatars.githubusercontent.com/u/207358?v=4?s=110" width="110px;" alt="Peter Sarossy"/><br /><sub><b>Peter Sarossy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=psarossy" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nepella"><img src="https://avatars.githubusercontent.com/u/11823649?v=4?s=110" width="110px;" alt="Renee Margaret McConahy"/><br /><sub><b>Renee Margaret McConahy</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nepella" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JohnnyPicnic"><img src="https://avatars.githubusercontent.com/u/5553884?v=4?s=110" width="110px;" alt="JohnnyPicnic"/><br /><sub><b>JohnnyPicnic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=JohnnyPicnic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/markbrule"><img src="https://avatars.githubusercontent.com/u/8799594?v=4?s=110" width="110px;" alt="markbrule"/><br /><sub><b>markbrule</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=markbrule" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikecmpbll"><img src="https://avatars.githubusercontent.com/u/1962801?v=4?s=110" width="110px;" alt="Mike Campbell"/><br /><sub><b>Mike Campbell</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mikecmpbll" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tbrconnect"><img src="https://avatars.githubusercontent.com/u/11973217?v=4?s=110" width="110px;" alt="tbrconnect"/><br /><sub><b>tbrconnect</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=tbrconnect" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kcoyo"><img src="https://avatars.githubusercontent.com/u/12447225?v=4?s=110" width="110px;" alt="kcoyo"/><br /><sub><b>kcoyo</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kcoyo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://travismiller.com/"><img src="https://avatars.githubusercontent.com/u/494017?v=4?s=110" width="110px;" alt="Travis Miller"/><br /><sub><b>Travis Miller</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=travismiller" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Delta5"><img src="https://avatars.githubusercontent.com/u/1975640?v=4?s=110" width="110px;" alt="Evan Taylor"/><br /><sub><b>Evan Taylor</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Delta5" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PetriAsi"><img src="https://avatars.githubusercontent.com/u/8735148?v=4?s=110" width="110px;" alt="Petri Asikainen"/><br /><sub><b>Petri Asikainen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PetriAsi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/derdeagle"><img src="https://avatars.githubusercontent.com/u/11424540?v=4?s=110" width="110px;" alt="derdeagle"/><br /><sub><b>derdeagle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=derdeagle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://wh0rd.org/"><img src="https://avatars.githubusercontent.com/u/176950?v=4?s=110" width="110px;" alt="Mike Frysinger"/><br /><sub><b>Mike Frysinger</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vapier" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AL4AL"><img src="https://avatars.githubusercontent.com/u/22044358?v=4?s=110" width="110px;" alt="ALPHA"/><br /><sub><b>ALPHA</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=AL4AL" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.ifern.de"><img src="https://avatars.githubusercontent.com/u/1042587?v=4?s=110" width="110px;" alt="FliegenKLATSCH"/><br /><sub><b>FliegenKLATSCH</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jerm"><img src="https://avatars.githubusercontent.com/u/442138?v=4?s=110" width="110px;" alt="Jeremy Price"/><br /><sub><b>Jeremy Price</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jerm" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Toreg87"><img src="https://avatars.githubusercontent.com/u/84392209?v=4?s=110" width="110px;" alt="Toreg87"/><br /><sub><b>Toreg87</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Toreg87" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Computroniks"><img src="https://avatars.githubusercontent.com/u/67638596?v=4?s=110" width="110px;" alt="Matthew Nickson"/><br /><sub><b>Matthew Nickson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Computroniks" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://jethron.id.au"><img src="https://avatars.githubusercontent.com/u/1646397?v=4?s=110" width="110px;" alt="Jethro Nederhof"/><br /><sub><b>Jethro Nederhof</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jethron" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/01ste02"><img src="https://avatars.githubusercontent.com/u/23289826?v=4?s=110" width="110px;" alt="Oskar Stenberg"/><br /><sub><b>Oskar Stenberg</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=01ste02" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Robert-Azelis"><img src="https://avatars.githubusercontent.com/u/82208283?v=4?s=110" width="110px;" alt="Robert-Azelis"/><br /><sub><b>Robert-Azelis</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Robert-Azelis" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alwism"><img src="https://avatars.githubusercontent.com/u/60648387?v=4?s=110" width="110px;" alt="Alexander William Smith"/><br /><sub><b>Alexander William Smith</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=alwism" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.leitwerk.de/"><img src="https://avatars.githubusercontent.com/u/24418301?v=4?s=110" width="110px;" alt="LEITWERK AG"/><br /><sub><b>LEITWERK AG</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=leitwerk-ag" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.aboutcher.co.uk"><img src="https://avatars.githubusercontent.com/u/1911435?v=4?s=110" width="110px;" alt="Adam"/><br /><sub><b>Adam</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adamboutcher" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://snksrv.com"><img src="https://avatars.githubusercontent.com/u/16104273?v=4?s=110" width="110px;" alt="Ian"/><br /><sub><b>Ian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sneak-it" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://blog.bestlong.idv.tw/"><img src="https://avatars.githubusercontent.com/u/4023909?v=4?s=110" width="110px;" alt="Shao Yu-Lung (Allen)"/><br /><sub><b>Shao Yu-Lung (Allen)</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bestlong" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Haxatron"><img src="https://avatars.githubusercontent.com/u/76475453?v=4?s=110" width="110px;" alt="Haxatron"/><br /><sub><b>Haxatron</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Haxatron" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PlaneNuts"><img src="https://avatars.githubusercontent.com/u/88776392?v=4?s=110" width="110px;" alt="PlaneNuts"/><br /><sub><b>PlaneNuts</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PlaneNuts" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://bjcpgd.cias.rit.edu"><img src="https://avatars.githubusercontent.com/u/3842948?v=4?s=110" width="110px;" alt="Bradley Coudriet"/><br /><sub><b>Bradley Coudriet</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=exula" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://daltondur.st"><img src="https://avatars.githubusercontent.com/u/21966173?v=4?s=110" width="110px;" alt="Dalton Durst"/><br /><sub><b>Dalton Durst</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=UniversalSuperBox" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://adagiohealth.org"><img src="https://avatars.githubusercontent.com/u/38761237?v=4?s=110" width="110px;" alt="Alex Janes"/><br /><sub><b>Alex Janes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=adagioajanes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nuraeil"><img src="https://avatars.githubusercontent.com/u/32387849?v=4?s=110" width="110px;" alt="Nuraeil"/><br /><sub><b>Nuraeil</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nuraeil" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TenOfTens"><img src="https://avatars.githubusercontent.com/u/48162670?v=4?s=110" width="110px;" alt="TenOfTens"/><br /><sub><b>TenOfTens</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=TenOfTens" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ditisjens.be/"><img src="https://avatars.githubusercontent.com/u/9415391?v=4?s=110" width="110px;" alt="waffle"/><br /><sub><b>waffle</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=insert-waffle" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QveenSi"><img src="https://avatars.githubusercontent.com/u/19945501?v=4?s=110" width="110px;" alt="Yevhenii Huzii"/><br /><sub><b>Yevhenii Huzii</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=QveenSi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/veenone"><img src="https://avatars.githubusercontent.com/u/3839381?v=4?s=110" width="110px;" alt="Achmad Fienan Rahardianto"/><br /><sub><b>Achmad Fienan Rahardianto</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=veenone" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/QveenSi"><img src="https://avatars.githubusercontent.com/u/19945501?v=4?s=110" width="110px;" alt="Yevhenii Huzii"/><br /><sub><b>Yevhenii Huzii</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=QveenSi" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chrisweirich"><img src="https://avatars.githubusercontent.com/u/97299851?v=4?s=110" width="110px;" alt="Christian Weirich"/><br /><sub><b>Christian Weirich</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chrisweirich" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/denzfarid"><img src="https://avatars.githubusercontent.com/u/1294403?v=4?s=110" width="110px;" alt="denzfarid"/><br /><sub><b>denzfarid</b></sub></a><br /></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ntbutler-nbcs"><img src="https://avatars.githubusercontent.com/u/94018771?v=4?s=110" width="110px;" alt="ntbutler-nbcs"/><br /><sub><b>ntbutler-nbcs</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ntbutler-nbcs" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://naveensrinivasan.dev"><img src="https://avatars.githubusercontent.com/u/172697?v=4?s=110" width="110px;" alt="Naveen"/><br /><sub><b>Naveen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=naveensrinivasan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mikeroq"><img src="https://avatars.githubusercontent.com/u/55674383?v=4?s=110" width="110px;" alt="Mike Roquemore"/><br /><sub><b>Mike Roquemore</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mikeroq" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/reederda"><img src="https://avatars.githubusercontent.com/u/7991086?v=4?s=110" width="110px;" alt="Daniel Reeder"/><br /><sub><b>Daniel Reeder</b></sub></a><br /><a href="#translation-reederda" title="Translation">🌍</a> <a href="#translation-reederda" title="Translation">🌍</a> <a href="https://github.com/snipe/snipe-it/commits?author=reederda" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vickyjaura183"><img src="https://avatars.githubusercontent.com/u/109422491?v=4?s=110" width="110px;" alt="vickyjaura183"/><br /><sub><b>vickyjaura183</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vickyjaura183" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/julian-piehl"><img src="https://avatars.githubusercontent.com/u/32363424?v=4?s=110" width="110px;" alt="Peace"/><br /><sub><b>Peace</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=julian-piehl" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kylegordon"><img src="https://avatars.githubusercontent.com/u/231528?v=4?s=110" width="110px;" alt="Kyle Gordon"/><br /><sub><b>Kyle Gordon</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kylegordon" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.bfh.ch"><img src="https://avatars.githubusercontent.com/u/53009155?v=4?s=110" width="110px;" alt="Katharina Drexel"/><br /><sub><b>Katharina Drexel</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=sunflowerbofh" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://david.sferruzza.fr/"><img src="https://avatars.githubusercontent.com/u/1931963?v=4?s=110" width="110px;" alt="David Sferruzza"/><br /><sub><b>David Sferruzza</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dsferruzza" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rnelsonee"><img src="https://avatars.githubusercontent.com/u/19511639?v=4?s=110" width="110px;" alt="Rick Nelson"/><br /><sub><b>Rick Nelson</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=rnelsonee" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BasO12"><img src="https://avatars.githubusercontent.com/u/94169344?v=4?s=110" width="110px;" alt="BasO12"/><br /><sub><b>BasO12</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=BasO12" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Vautia"><img src="https://avatars.githubusercontent.com/u/111710123?v=4?s=110" width="110px;" alt="Vautia"/><br /><sub><b>Vautia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Vautia" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.littlehart.net/atthekeyboard"><img src="https://avatars.githubusercontent.com/u/28321?v=4?s=110" width="110px;" alt="Chris Hartjes"/><br /><sub><b>Chris Hartjes</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chartjes" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geo-chen"><img src="https://avatars.githubusercontent.com/u/2404584?v=4?s=110" width="110px;" alt="geo-chen"/><br /><sub><b>geo-chen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=geo-chen" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nh314"><img src="https://avatars.githubusercontent.com/u/6006620?v=4?s=110" width="110px;" alt="Phan Nguyen"/><br /><sub><b>Phan Nguyen</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=nh314" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/StarlessNights"><img src="https://avatars.githubusercontent.com/u/115993812?v=4?s=110" width="110px;" alt="Iisakki Jaakkola"/><br /><sub><b>Iisakki Jaakkola</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=StarlessNights" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=110" width="110px;" alt="Ikko Ashimine"/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=eltociear" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lukasfehling"><img src="https://avatars.githubusercontent.com/u/56871540?v=4?s=110" width="110px;" alt="Lukas Fehling"/><br /><sub><b>Lukas Fehling</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=lukasfehling" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fernando-almeida"><img src="https://avatars.githubusercontent.com/u/1975990?v=4?s=110" width="110px;" alt="Fernando Almeida"/><br /><sub><b>Fernando Almeida</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=fernando-almeida" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/akemidx"><img src="https://avatars.githubusercontent.com/u/116301219?v=4?s=110" width="110px;" alt="akemidx"/><br /><sub><b>akemidx</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=akemidx" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://oguz.site"><img src="https://avatars.githubusercontent.com/u/144778?v=4?s=110" width="110px;" alt="Oguz Bilgic"/><br /><sub><b>Oguz Bilgic</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=oguzbilgic" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/scoo73r"><img src="https://avatars.githubusercontent.com/u/9262438?v=4?s=110" width="110px;" alt="Scooter Crawford"/><br /><sub><b>Scooter Crawford</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=scoo73r" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/subdriven"><img src="https://avatars.githubusercontent.com/u/5957345?v=4?s=110" width="110px;" alt="subdriven"/><br /><sub><b>subdriven</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=subdriven" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/AndrewSav"><img src="https://avatars.githubusercontent.com/u/658865?v=4?s=110" width="110px;" alt="Andrew Savinykh"/><br /><sub><b>Andrew Savinykh</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=AndrewSav" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://kenchan0130.github.io"><img src="https://avatars.githubusercontent.com/u/1155067?v=4?s=110" width="110px;" alt="Tadayuki Onishi"/><br /><sub><b>Tadayuki Onishi</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=kenchan0130" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/floschoepfer"><img src="https://avatars.githubusercontent.com/u/112496896?v=4?s=110" width="110px;" alt="Florian"/><br /><sub><b>Florian</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=floschoepfer" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://spencerlong.com"><img src="https://avatars.githubusercontent.com/u/7305753?v=4?s=110" width="110px;" alt="Spencer Long"/><br /><sub><b>Spencer Long</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=spencerrlongg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marcusmoore"><img src="https://avatars.githubusercontent.com/u/1141514?v=4?s=110" width="110px;" alt="Marcus Moore"/><br /><sub><b>Marcus Moore</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=marcusmoore" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Mezzle"><img src="https://avatars.githubusercontent.com/u/570639?v=4?s=110" width="110px;" alt="Martin Meredith"/><br /><sub><b>Martin Meredith</b></sub></a><br /></td>
<td align="center" valign="top" width="14.28%"><a href="http://dboth.de"><img src="https://avatars.githubusercontent.com/u/5731963?v=4?s=110" width="110px;" alt="dboth"/><br /><sub><b>dboth</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=dboth" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zacharyfleck"><img src="https://avatars.githubusercontent.com/u/87536651?v=4?s=110" width="110px;" alt="Zachary Fleck"/><br /><sub><b>Zachary Fleck</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=zacharyfleck" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vikaas-cyper"><img src="https://avatars.githubusercontent.com/u/74609912?v=4?s=110" width="110px;" alt="VIKAAS-A"/><br /><sub><b>VIKAAS-A</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=vikaas-cyper" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ak-piracha"><img src="https://avatars.githubusercontent.com/u/88882041?v=4?s=110" width="110px;" alt="Abdul Kareem"/><br /><sub><b>Abdul Kareem</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=ak-piracha" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NojoudAlshehri"><img src="https://avatars.githubusercontent.com/u/111287779?v=4?s=110" width="110px;" alt="NojoudAlshehri"/><br /><sub><b>NojoudAlshehri</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stefanstidlffg"><img src="https://avatars.githubusercontent.com/u/54367449?v=4?s=110" width="110px;" alt="Stefan Stidl"/><br /><sub><b>Stefan Stidl</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=stefanstidlffg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qay21"><img src="https://avatars.githubusercontent.com/u/87803479?v=4?s=110" width="110px;" alt="Quentin Aymard"/><br /><sub><b>Quentin Aymard</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=qay21" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cram42"><img src="https://avatars.githubusercontent.com/u/5396871?v=4?s=110" width="110px;" alt="Grant Le Roux"/><br /><sub><b>Grant Le Roux</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=cram42" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://@singrity"><img src="https://avatars.githubusercontent.com/u/58479551?v=4?s=110" width="110px;" alt="Bogdan"/><br /><sub><b>Bogdan</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Singrity" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mmanjos"><img src="https://avatars.githubusercontent.com/u/3483684?v=4?s=110" width="110px;" alt="mmanjos"/><br /><sub><b>mmanjos</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mmanjos" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://azooz2014.github.io/"><img src="https://avatars.githubusercontent.com/u/7429229?v=4?s=110" width="110px;" alt="Abdelaziz Faki"/><br /><sub><b>Abdelaziz Faki</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Azooz2014" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bilias"><img src="https://avatars.githubusercontent.com/u/47315739?v=4?s=110" width="110px;" alt="bilias"/><br /><sub><b>bilias</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bilias" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/coach1988"><img src="https://avatars.githubusercontent.com/u/2565989?v=4?s=110" width="110px;" alt="coach1988"/><br /><sub><b>coach1988</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=coach1988" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mauro-miatello"><img src="https://avatars.githubusercontent.com/u/11910225?v=4?s=110" width="110px;" alt="MrM"/><br /><sub><b>MrM</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mauro-miatello" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/koiakoia"><img src="https://avatars.githubusercontent.com/u/60405354?v=4?s=110" width="110px;" alt="koiakoia"/><br /><sub><b>koiakoia</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=koiakoia" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mustafa-online"><img src="https://avatars.githubusercontent.com/u/5323832?v=4?s=110" width="110px;" alt="Mustafa Online"/><br /><sub><b>Mustafa Online</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=mustafa-online" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/franceslui"><img src="https://avatars.githubusercontent.com/u/104601439?v=4?s=110" width="110px;" alt="franceslui"/><br /><sub><b>franceslui</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=franceslui" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Q4kK"><img src="https://avatars.githubusercontent.com/u/125313163?v=4?s=110" width="110px;" alt="Q4kK"/><br /><sub><b>Q4kK</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Q4kK" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/squintfox"><img src="https://avatars.githubusercontent.com/u/55590532?v=4?s=110" width="110px;" alt="squintfox"/><br /><sub><b>squintfox</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=squintfox" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jeffclay"><img src="https://avatars.githubusercontent.com/u/1380084?v=4?s=110" width="110px;" alt="Jeff Clay"/><br /><sub><b>Jeff Clay</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=jeffclay" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PP-JN-RL"><img src="https://avatars.githubusercontent.com/u/52716446?v=4?s=110" width="110px;" alt="Phil J R"/><br /><sub><b>Phil J R</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=PP-JN-RL" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.corelight.com/"><img src="https://avatars.githubusercontent.com/u/1496725?v=4?s=110" width="110px;" alt="i_virus"/><br /><sub><b>i_virus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chandanchowdhury" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gitgrimbo"><img src="https://avatars.githubusercontent.com/u/1020541?v=4?s=110" width="110px;" alt="Paul Grime"/><br /><sub><b>Paul Grime</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gitgrimbo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://leeporte.co.uk"><img src="https://avatars.githubusercontent.com/u/922815?v=4?s=110" width="110px;" alt="Lee Porte"/><br /><sub><b>Lee Porte</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=LeePorte" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bryanlopezinc"><img src="https://avatars.githubusercontent.com/u/23613427?v=4?s=110" width="110px;" alt="BRYAN "/><br /><sub><b>BRYAN </b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bryanlopezinc" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=bryanlopezinc" title="Tests">⚠️</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/U-H-T"><img src="https://avatars.githubusercontent.com/u/64061710?v=4?s=110" width="110px;" alt="U-H-T"/><br /><sub><b>U-H-T</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=U-H-T" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tyree"><img src="https://avatars.githubusercontent.com/u/5395363?v=4?s=110" width="110px;" alt="Matt Tyree"/><br /><sub><b>Matt Tyree</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Tyree" title="Documentation">📖</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
-144
View File
@@ -1,144 +0,0 @@
FROM ubuntu:22.04
LABEL maintainer="Brady Wetherington <bwetherington@grokability.com>"
# No need to add `apt-get clean` here, reference:
# - https://github.com/snipe/snipe-it/pull/9201
# - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#apt-get
RUN export DEBIAN_FRONTEND=noninteractive; \
export DEBCONF_NONINTERACTIVE_SEEN=true; \
echo 'tzdata tzdata/Areas select Etc' | debconf-set-selections; \
echo 'tzdata tzdata/Zones/Etc select UTC' | debconf-set-selections; \
apt-get update -qqy \
&& apt-get install -qqy --no-install-recommends \
apt-utils \
apache2 \
apache2-bin \
libapache2-mod-php8.1 \
php8.1-curl \
php8.1-ldap \
php8.1-mysql \
php8.1-gd \
php8.1-xml \
php8.1-mbstring \
php8.1-zip \
php8.1-bcmath \
php8.1-redis \
php-memcached \
patch \
curl \
wget \
vim \
git \
cron \
mysql-client \
supervisor \
cron \
gcc \
make \
autoconf \
libc-dev \
libldap-common \
pkg-config \
libmcrypt-dev \
php8.1-dev \
ca-certificates \
unzip \
dnsutils \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN curl -L -O https://github.com/pear/pearweb_phars/raw/master/go-pear.phar
RUN php go-pear.phar
RUN pecl install mcrypt
RUN bash -c "echo extension=/usr/lib/php/20210902/mcrypt.so > /etc/php/8.1/mods-available/mcrypt.ini"
RUN phpenmod mcrypt
RUN phpenmod gd
RUN phpenmod bcmath
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.1/apache2/php.ini
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php/8.1/cli/php.ini
RUN useradd -m --uid 1000 --gid 50 docker
RUN echo export APACHE_RUN_USER=docker >> /etc/apache2/envvars
RUN echo export APACHE_RUN_GROUP=staff >> /etc/apache2/envvars
COPY docker/000-default.conf /etc/apache2/sites-enabled/000-default.conf
#SSL
RUN mkdir -p /var/lib/snipeit/ssl
#COPY docker/001-default-ssl.conf /etc/apache2/sites-enabled/001-default-ssl.conf
COPY docker/001-default-ssl.conf /etc/apache2/sites-available/001-default-ssl.conf
RUN a2enmod ssl
RUN a2ensite 001-default-ssl.conf
COPY . /var/www/html
RUN a2enmod rewrite
COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
############ INITIAL APPLICATION SETUP #####################
WORKDIR /var/www/html
#Append to bootstrap file (less brittle than 'patch')
# RUN sed -i 's/return $app;/$env="production";\nreturn $app;/' bootstrap/start.php
#copy all configuration files
# COPY docker/*.php /var/www/html/app/config/production/
COPY docker/docker.env /var/www/html/.env
RUN chown -R docker /var/www/html
RUN \
rm -r "/var/www/html/storage/private_uploads" && ln -fs "/var/lib/snipeit/data/private_uploads" "/var/www/html/storage/private_uploads" \
&& rm -rf "/var/www/html/public/uploads" && ln -fs "/var/lib/snipeit/data/uploads" "/var/www/html/public/uploads" \
&& rm -r "/var/www/html/storage/app/backups" && ln -fs "/var/lib/snipeit/dumps" "/var/www/html/storage/app/backups" \
&& mkdir -p "/var/lib/snipeit/keys" && ln -fs "/var/lib/snipeit/keys/oauth-private.key" "/var/www/html/storage/oauth-private.key" \
&& ln -fs "/var/lib/snipeit/keys/oauth-public.key" "/var/www/html/storage/oauth-public.key" \
&& ln -fs "/var/lib/snipeit/keys/ldap_client_tls.cert" "/var/www/html/storage/ldap_client_tls.cert" \
&& ln -fs "/var/lib/snipeit/keys/ldap_client_tls.key" "/var/www/html/storage/ldap_client_tls.key" \
&& chown docker "/var/lib/snipeit/keys/" \
&& chown -Rh docker "/var/www/html/storage/" \
&& chmod +x /var/www/html/artisan \
&& echo "Finished setting up application in /var/www/html"
############## DEPENDENCIES via COMPOSER ###################
#global install of composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Get dependencies
USER docker
RUN composer install --no-dev --working-dir=/var/www/html
USER root
############### APPLICATION INSTALL/INIT #################
#RUN php artisan app:install
# too interactive! Try something else
#COPY docker/app_install.exp /tmp/app_install.exp
#RUN chmod +x /tmp/app_install.exp
#RUN /tmp/app_install.exp
############### DATA VOLUME #################
VOLUME ["/var/lib/snipeit"]
##### START SERVER
COPY docker/startup.sh docker/supervisord.conf /
COPY docker/supervisor-exit-event-listener /usr/bin/supervisor-exit-event-listener
RUN chmod +x /startup.sh /usr/bin/supervisor-exit-event-listener
CMD ["/startup.sh"]
EXPOSE 80
EXPOSE 443
-90
View File
@@ -1,90 +0,0 @@
FROM alpine:3.18.6
# Apache + PHP
RUN apk add --no-cache \
apache2 \
php81 \
php81-common \
php81-apache2 \
php81-curl \
php81-ldap \
php81-mysqli \
php81-gd \
php81-xml \
php81-mbstring \
php81-zip \
php81-ctype \
php81-tokenizer \
php81-pdo_mysql \
php81-openssl \
php81-bcmath \
php81-phar \
php81-json \
php81-iconv \
php81-fileinfo \
php81-simplexml \
php81-session \
php81-dom \
php81-xmlwriter \
php81-xmlreader \
php81-sodium \
php81-redis \
php81-pecl-memcached \
php81-exif \
curl \
wget \
vim \
git \
mysql-client \
tini
COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
# Where apache's PID lives
RUN mkdir -p /run/apache2 && chown apache:apache /run/apache2
RUN sed -i 's/variables_order = .*/variables_order = "EGPCS"/' /etc/php81/php.ini
COPY docker/000-default-2.4.conf /etc/apache2/conf.d/default.conf
# Enable mod_rewrite
RUN sed -i '/LoadModule rewrite_module/s/^#//g' /etc/apache2/httpd.conf
COPY . /var/www/html
WORKDIR /var/www/html
COPY docker/docker.env /var/www/html/.env
RUN chown -R apache:apache /var/www/html
RUN \
rm -r "/var/www/html/storage/private_uploads" \
&& mkdir -p "/var/lib/snipeit/data/private_uploads" && ln -fs "/var/lib/snipeit/data/private_uploads" "/var/www/html/storage/private_uploads" \
&& rm -rf "/var/www/html/public/uploads" \
&& mkdir -p "/var/lib/snipeit/data/uploads" && ln -fs "/var/lib/snipeit/data/uploads" "/var/www/html/public/uploads" \
&& mkdir -p "/var/lib/snipeit/dumps" && rm -r "/var/www/html/storage/app/backups" && ln -fs "/var/lib/snipeit/dumps" "/var/www/html/storage/app/backups" \
&& mkdir -p "/var/lib/snipeit/keys" && ln -fs "/var/lib/snipeit/keys/oauth-private.key" "/var/www/html/storage/oauth-private.key" \
&& ln -fs "/var/lib/snipeit/keys/oauth-public.key" "/var/www/html/storage/oauth-public.key" \
&& chown -hR apache "/var/www/html/storage/" \
&& chown -R apache "/var/lib/snipeit"
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN mkdir -p /var/www/.composer && chown apache /var/www/.composer
# Install dependencies
USER apache
RUN COMPOSER_CACHE_DIR=/dev/null composer install --no-dev --working-dir=/var/www/html
USER root
VOLUME ["/var/lib/snipeit"]
# Entrypoints
COPY docker/entrypoint_alpine.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/entrypoint.sh"]
EXPOSE 80
-103
View File
@@ -1,103 +0,0 @@
ARG ENVIRONMENT=production
ARG SNIPEIT_RELEASE=6.1.0
ARG PHP_VERSION=8.2
ARG PHP_ALPINE_VERSION=3.17
ARG COMPOSER_VERSION=2
# Cannot use arguments with 'COPY --from' workaround
# https://github.com/moby/moby/issues/34482#issuecomment-454716952
FROM composer:${COMPOSER_VERSION} AS composer
# Final stage
FROM php:${PHP_VERSION}-fpm-alpine${PHP_ALPINE_VERSION} AS source
LABEL maintainer="Mateus Villar <mromeravillar@gmail.com>"
ARG PACKAGES="\
mysql-client \
"
ARG DEV_PACKAGES="\
git \
"
ARG ENVIRONMENT
ENV ENVIRONMENT ${ENVIRONMENT}
ARG SNIPEIT_RELEASE
ENV SNIPEIT_RELEASE ${SNIPEIT_RELEASE}
# Cribbed from wordpress-fpm-alpine image
# set recommended PHP.ini settings
# see https://secure.php.net/manual/en/opcache.installation.php
RUN set -eux; \
docker-php-ext-enable opcache; \
{ \
echo 'opcache.memory_consumption=128'; \
echo 'opcache.interned_strings_buffer=8'; \
echo 'opcache.max_accelerated_files=4000'; \
echo 'opcache.revalidate_freq=2'; \
echo 'opcache.fast_shutdown=1'; \
} > /usr/local/etc/php/conf.d/opcache-recommended.ini
# https://wordpress.org/support/article/editing-wp-config-php/#configure-error-logging
RUN { \
# https://www.php.net/manual/en/errorfunc.constants.php
# https://github.com/docker-library/wordpress/issues/420#issuecomment-517839670
echo 'error_reporting = E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING | E_RECOVERABLE_ERROR'; \
echo 'display_errors = Off'; \
echo 'display_startup_errors = Off'; \
echo 'log_errors = On'; \
echo 'error_log = /dev/stderr'; \
echo 'log_errors_max_len = 1024'; \
echo 'ignore_repeated_errors = On'; \
echo 'ignore_repeated_source = Off'; \
echo 'html_errors = Off'; \
} > /usr/local/etc/php/conf.d/error-logging.ini
# Install php extensions inside docker containers easily
# https://github.com/mlocati/docker-php-extension-installer
COPY --from=mlocati/php-extension-installer:2.1.15 /usr/bin/install-php-extensions /usr/local/bin/
RUN set -eux; \
install-php-extensions \
bcmath \
gd \
ldap \
mysqli \
pdo_mysql \
zip; \
rm -f /usr/local/bin/install-php-extensions; \
# Install prerequisites packages
apk add --no-cache \
${PACKAGES};
COPY --from=composer /usr/bin/composer /usr/local/bin
ARG COMPOSER_ALLOW_SUPERUSER=1
RUN set -eux; \
# Download and extract snipeit tarball
curl -o snipeit.tar.gz -fL "https://github.com/snipe/snipe-it/archive/v$SNIPEIT_RELEASE.tar.gz"; \
tar -xzf snipeit.tar.gz --strip-components=1 -C /var/www/html/; \
rm snipeit.tar.gz; \
# Install composer php dependencies
if [ "$ENVIRONMENT" = "production" ]; then \
echo "production environment detected!"; \
composer update \
--no-cache \
--no-dev \
--optimize-autoloader \
--working-dir=/var/www/html; \
else \
echo "development environment detected!"; \
apk add --no-cache \
${DEV_PACKAGES}; \
composer update \
--no-cache \
--prefer-source \
--optimize-autoloader \
--working-dir=/var/www/html; \
fi; \
rm -f /usr/local/bin/composer; \
chown -R www-data:www-data /var/www/html;
VOLUME [ "/var/lib/snipeit" ]
COPY --chown=www-data:www-data docker/docker-secrets.env /var/www/html/.env
COPY --chmod=655 docker/docker-entrypoint.sh /usr/local/bin/docker-snipeit-entrypoint
COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
ENTRYPOINT [ "/usr/local/bin/docker-snipeit-entrypoint" ]
CMD [ "/usr/local/bin/docker-php-entrypoint", "php-fpm" ]
Executable → Regular
View File
-1
View File
@@ -1 +0,0 @@
web: php heroku/startup.php && heroku-php-apache2 public/
+148 -72
View File
@@ -1,99 +1,175 @@
![snipe-it-by-grok](https://github.com/snipe/snipe-it/assets/197404/b515673b-c7c8-4d9a-80f5-9fa58829a602)
<a href='https://pledgie.com/campaigns/22899'><img alt='Click here to lend your support to: Snipe IT - Free Open Source Asset Management System and make a donation at pledgie.com !' src='https://pledgie.com/campaigns/22899.png?skin_name=chrome' border='0' ></a>
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Twitter Follow](https://img.shields.io/twitter/follow/snipeitapp.svg?style=social)](https://twitter.com/snipeitapp) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://app.codacy.com/gh/snipe/snipe-it/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Tests](https://github.com/snipe/snipe-it/actions/workflows/tests.yml/badge.svg)](https://github.com/snipe/snipe-it/actions/workflows/tests.yml)
[![All Contributors](https://img.shields.io/badge/all_contributors-331-orange.svg?style=flat-square)](#contributing) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk)
## Snipe-IT - Open Source Asset Management System
## Snipe-IT - Asset Management For the Rest of Us
This is a FOSS project for asset management in IT Operations. Knowing who has which laptop, when it was purchased in order to depreciate it correctly, handling software licenses, etc.
It is built on [Laravel 10](http://laravel.com).
It is built on [Laravel 4](http://laravel.com) and uses the [Sentry 2](https://github.com/cartalyst/sentry) package.
Snipe-IT is actively developed and we [release quite frequently](https://github.com/snipe/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).)
Many thanks to the [Laravel 4 starter site](https://github.com/brunogaspar/laravel4-starter-kit) for a quick start.
> [!TIP]
> __This is web-based software__. This means there is no executable file (aka no .exe files), and it must be run on a web server and accessed through a web browser. It runs on any Mac OSX, any flavor of Linux, as well as Windows, and we have a [Docker image](https://snipe-it.readme.io/docs/docker) available if that's what you're into.
This isn't actually ready for anyone to use yet, as I'm still working out some of the basic functionality. Feel free to check out the [GitHub Issues for this project](https://github.com/snipe/snipe-it/issues) to check on progress, open a bug report, or see what open issues you can help with.
-----
### Installation
## Requirements
For instructions on installing and configuring Snipe-IT on your server, check out the [installation manual](https://snipe-it.readme.io/docs). (Please see the [requirements documentation](https://snipe-it.readme.io/docs/requirements) for full requirements.)
If you're having trouble with the installation, please check the [Common Issues](https://snipe-it.readme.io/docs/common-issues) and [Getting Help](https://snipe-it.readme.io/docs/getting-help) documentation, and search this repository's open *and* closed issues for help.
<!-- [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) -->
-----
### User's Manual
For help using Snipe-IT, check out the [user's manual](https://snipe-it.readme.io/docs/overview).
-----
### Bug Reports & Feature Requests
Feel free to check out the [GitHub Issues for this project](https://github.com/snipe/snipe-it/issues) to open a bug report or see what open issues you can help with. Please search through existing issues (open *and* closed) to see if your question has already been answered before opening a new issue.
> [!IMPORTANT]
> **PLEASE see the [Getting Help Guidelines](https://snipe-it.readme.io/docs/getting-help) and [Common Issues](https://snipe-it.readme.io/docs/common-issues) before opening a ticket, and be sure to complete all of the questions in the Github Issue template to help us to help you as quickly as possible.**
>
-----
### Upgrading
Please see the [upgrading documentation](https://snipe-it.readme.io/docs/upgrading) for instructions on upgrading Snipe-IT.
------
### Announcement List
To be notified of important news (such as new releases, security advisories, etc), [sign up for our list](http://eepurl.com/XyZKz). We'll never sell or give away your info, and we'll only email you when it's important.
------
### Translations!
Please see the [translations documentation](https://snipe-it.readme.io/docs/translations) for information about available languages and how to add translations to Snipe-IT.
- PHP 5.3.7 or later
- MCrypt PHP Extension
-----
### Libraries, Modules & Related Projects
## Important Note on Updating
Since the release of the JSON REST API, several third-party developers have been developing modules and libraries to work with Snipe-IT.
Whenever you pull down a new version from master or develop, when you grab the [latest official release](https://github.com/snipe/snipe-it/releases), make sure to run the following commands via command line:
> [!NOTE]
> As these were created by third-parties, Snipe-IT cannot provide support for these project, and you should contact the developers directly if you need assistance. Additionally, Snipe-IT makes no guarantees as to the reliability, accuracy or maintainability of these libraries. Use at your own risk. :)
php composer.phar dump-autoload
php artisan migrate
- [Python Module](https://github.com/jbloomer/SnipeIT-PythonAPI) by [@jbloomer](https://github.com/jbloomer)
- [SnipeSharp - .NET module in C#](https://github.com/barrycarey/SnipeSharp) by [@barrycarey](https://github.com/barrycarey)
- [InQRy -unmaintained-](https://github.com/Microsoft/InQRy) by [@Microsoft](https://github.com/Microsoft)
- [SnipeitPS](https://github.com/snazy2000/SnipeitPS) by [@snazy2000](https://github.com/snazy2000) - Powershell API Wrapper for Snipe-it
- [jamf2snipe](https://github.com/grokability/jamf2snipe) - Python script to sync assets between a JAMFPro instance and a Snipe-IT instance
- [jamf-snipe-rename](https://macblog.org/jamf-snipe-rename/) - Python script to rename computers in Jamf from Snipe-IT
- [Marksman](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
- [Snipe-IT plugin for Jira Service Desk](https://marketplace.atlassian.com/apps/1220964/snipe-it-for-jira)
- [Python 3 CSV importer](https://github.com/gastamper/snipeit-csvimporter) - allows importing assets into Snipe-IT based on Item Name rather than Asset Tag.
- [Snipe-IT Kubernetes Helm Chart](https://github.com/t3n/helm-charts/tree/master/snipeit) - For more information, [click here](https://hub.helm.sh/charts/t3n/snipeit).
- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-it.
- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT
- [WWW::SnipeIT](https://github.com/SEDC/perl-www-snipeit) by [@SEDC](https://github.com/SEDC) - perl module for accessing the API
- [UniFi to Snipe-IT](https://github.com/RodneyLeeBrands/UnifiSnipeSync) by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT.
- [Kandji2Snipe](https://github.com/grokability/kandji2snipe) by [@briangoldstein](https://github.com/briangoldstein) - Python script that synchronizes Kandji with Snipe-IT.
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by @ReticentRobot - Windows agent for Snipe-IT
Forgetting to do this can mean your DB might end up out of sync with the new files you just pulled, or you may have some funky cached autoloader values. It's a good idea to get into the habit of running these every time you pull anything new down. If there are no database changes to migrate, it won't hurt anything to run migrations anyway.
-----
### Contributing
## How to Install
Please see the documentation on [contributing and developing for Snipe-IT](https://snipe-it.readme.io/docs/contributing-overview).
### 1) Downloading
#### 1.1) Clone the Repository
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
git clone http://github.com/snipe/snipe-it your-folder
The ERD is available [online here](https://drawsql.app/templates/snipe-it).
#### 1.2) Download the Repository
[Here is a list](CONTRIBUTORS.md) of the wonderful people that have contributed to the Snipe-IT.
https://github.com/snipe/snipe-it/archive/master.zip
-----
### Security
### 2) Install the Dependencies via Composer
##### 2.1) If you don't have composer installed globally
cd your-folder
curl -s http://getcomposer.org/installer | php
php composer.phar install
##### 2.2) For globally composer installations
cd your-folder
composer install
-----
### 3) Setup Database
Copy the file `app/config/database.php` to `database.php`, and update `database.php` with your database name and credentials
cp app/config/database.example.php app/config/database.php
vi app/config/database.example.php
-----
### 4) Setup Mail Settings
Now, copy the file `app/config/mail.php` to `mail.php`, and update `mail.php` with your mail settings
cp app/config/mail.example.php app/config/mail.php
vi app/config/mail.example.php
This will be used to send emails to your users, when they register and they request a password reset.
-----
### 5) Use custom CLI Installer Command
Now, you need to create yourself a user and finish the installation.
Use the following command to create your default user, user groups and run all the necessary migrations automatically.
php artisan app:install
-----
### 6) Fix permissions
You'll need to make sure that the app/storage directory is writable by your webserver, since caches and log files get written there. You should use the minimum permissions available for writing, based on how you've got your webserver configured.
chmod -R 755 app/storage
If you still run into a permissions error, you may need to increase the permissions to 775, or twiddle your user/group permissions on your server.
chmod -R 775 app/storage
-----
### 7) Set the correct document root for your server
The document root for the app should be set to the public directory. In a standard Apache virtualhost setup, that might look something like this:
<VirtualHost *:80>
DocumentRoot /var/www/html/public
ServerName www.example.org
# Other directives here
</VirtualHost>
-----
### 8) Seed the Database
Loading up the sample data will give you an idea of how this should look, how your info should be structured, etc. It only pre-loads a handful of items, so you won't have to spend an hour deleting sample data.
php artisan db:seed
-----
## Optional Development Stuff
### Set up the debugbar
In dev mode, I use the fabulous [Laravel Debugbar](https://github.com/barryvdh/laravel-debugbar) by @barryvdh. After you've installed/updated composer, you'll need to publish the assets for the debugbar:
php artisan debugbar:publish
The profiler is enabled by default if you have debug set to true in your app.php. You certainly don't have to use it, but it's pretty handy for troubleshooting queries, seeing how much memory your pages are using, etc.
-----
### Purging the autoloader
If you're doing any development on this, make sure you purge the auto-loader if you see any errors stating the new model you created can't be found, etc, otherwise your new models won't be grokked.
php composer.phar dump-autoload
-----
### Running this on an EC2 Micro Instance
Depending on your needs, you could probably run this system in an EC2 micro instance. It doesn't take up very much memory and typically won't be a super-high-traffic application. EC2 micros fall into the free/dirt-cheap tier, which might make this a nice option. One thing to note though - composer can be a little memory-intensive while you're running updates, and you may have trouble with it failing on a micro. You can crank the memory_limit up in php.ini, but EC2 micros have swap disabled by default, so even that may not cut it. If you run into totally unhelpful error messages while running composer updates (like simply 'Killed') or fatal memory issues mentioning phar, your best bet will be to enable swap:
sudo /bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=1024
sudo /sbin/mkswap /var/swap.1
sudo /sbin/swapon /var/swap.1
If you need more than 1024 then change that to something higher.
To enable it by default after reboot, add this line to /etc/fstab:
/var/swap.1 swap swap defaults 0 0
-----
## License
Copyright (C) 2013 Alison Gianotto - snipe@snipe.net
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
> [!IMPORTANT]
> **To report a security vulnerability, please email security@snipeitapp.com instead of using the issue tracker.**
-27
View File
@@ -1,27 +0,0 @@
# Security Policy
We take security issues very seriously, and will always attempt to address any
vulnerabilities as quickly as possible.
## Supported Versions
We try to make a reasonable effort to support older versions of Snipe-IT,
however there are times when library dependencies and/or PHP/MySQL dependencies
make it impossible to backport security fixes on older versions.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Security vulnerabilities should be sent to security@snipeitapp.com. You can typically expect a
response within two business days, and we typically have fixes out in under a week from the initial disclosure.
This obviously varies based on the severity of the security issue and the difficulty in remediation,
but those have historically been the timelines we worm around.
For a full breakdown of our security policies, please see https://snipeitapp.com/security.
-65
View File
@@ -1,65 +0,0 @@
# Running the Test Suite
This document is targeted at developers looking to make modifications to this application's code base and want to run the existing test suite.
Before starting, follow the [instructions](README.md#installation) for installing the application locally and ensure you can load it in a browser properly.
## Unit and Feature Tests
Before attempting to run the test suite copy the example environment file for tests and update the values to match your environment:
`cp .env.testing.example .env.testing`
The following should work for running tests in memory with sqlite:
```
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=testing
APP_DEBUG=true
APP_KEY=base64:glJpcM7BYwWiBggp3SQ/+NlRkqsBQMaGEOjemXqJzOU=
APP_URL=http://localhost:8000
APP_TIMEZONE='UTC'
APP_LOCALE=en
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=sqlite_testing
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=null
#DB_USERNAME=null
#DB_PASSWORD=null
```
To use MySQL you should update the `DB_` variables to match your local test database:
```
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE={}
DB_USERNAME={}
DB_PASSWORD={}
```
Now you are ready to run the entire test suite from your terminal:
```shell
php artisan test
````
To run individual test files, you can pass the path to the test that you want to run:
```shell
php artisan test tests/Unit/AccessoryTest.php
```
Some tests, like ones concerning LDAP, are marked with the `@group` annotation. Those groups can be run, or excluded, using the `--group` or `--exclude-group` flags:
```shell
php artisan test --group=ldap
php artisan test --exclude-group=ldap
```
This can be helpful if a set of tests are failing because you don't have an extension, like LDAP, installed.
Vendored
-111
View File
@@ -1,111 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
SNIPEIT_SH_URL= "https://raw.githubusercontent.com/snipe/snipe-it/master/snipeit.sh"
NETWORK_BRIDGE= "en0: Wi-Fi (AirPort)"
Vagrant.configure("2") do |config|
config.vm.define "bionic" do |bionic|
bionic.vm.box = "ubuntu/bionic64"
bionic.vm.hostname = 'bionic'
bionic.vm.network "forwarded_port", guest: 80, host: 8080
bionic.vm.synced_folder ".", "/vagrant", :owner => 'www-data',
:group => 'vagrant', :mount_options => ['dmode=775', 'fmode=775']
bionic.vm.provision "ansible_local" do |ansible|
ansible.playbook = "ansible/ubuntu/vagrant_playbook.yml"
end
end
config.vm.define "xenial" do |xenial|
xenial.vm.box = "ubuntu/xenial64"
xenial.vm.hostname = 'xenial'
xenial.vm.network "forwarded_port", guest: 80, host: 8080
xenial.vm.synced_folder ".", "/vagrant", :owner => 'www-data',
:group => 'vagrant', :mount_options => ['dmode=775', 'fmode=775']
xenial.vm.provision "ansible_local" do |ansible|
ansible.playbook = "ansible/ubuntu/vagrant_playbook.yml"
end
end
config.vm.define "trusty" do |trusty|
trusty.vm.box = "ubuntu/trusty32"
trusty.vm.hostname = 'trusty'
trusty.vm.network "forwarded_port", guest: 80, host: 8080
trusty.vm.synced_folder ".", "/vagrant", :owner => 'www-data',
:group => 'vagrant', :mount_options => ['dmode=775', 'fmode=775']
trusty.vm.provision "ansible_local" do |ansible|
ansible.playbook = "ansible/ubuntu/vagrant_playbook.yml"
end
end
config.vm.define "centos7" do |centos7|
centos7.vm.box = "centos/7"
centos7.vm.hostname = 'centos7'
centos7.vm.network "public_network", bridge: NETWORK_BRIDGE
centos7.vm.provision :shell, :inline => "sudo yum -y update"
centos7.vm.provision :shell, :inline => "yum install -y wget"
centos7.vm.provision :shell, :inline => "wget #{SNIPEIT_SH_URL}"
centos7.vm.provision :shell, :inline => "chmod 755 snipeit.sh"
end
config.vm.define "centos6" do |centos6|
centos6.vm.box = "centos/6"
centos6.vm.hostname = 'centos6'
centos6.vm.network "public_network", bridge: NETWORK_BRIDGE
centos6.vm.provision :shell, :inline => "sudo yum -y update"
centos6.vm.provision :shell, :inline => "wget #{SNIPEIT_SH_URL}"
centos6.vm.provision :shell, :inline => "chmod 755 snipeit.sh"
end
config.vm.define "jessie" do |jessie|
jessie.vm.box = "debian/jessie64"
jessie.vm.hostname = 'debian8'
jessie.vm.network "public_network", bridge: NETWORK_BRIDGE
jessie.vm.provision :shell, :inline => "wget #{SNIPEIT_SH_URL}"
jessie.vm.provision :shell, :inline => "chmod 755 snipeit.sh"
end
config.vm.define "stretch" do |stretch|
stretch.vm.box = "debian/stretch64"
stretch.vm.hostname = 'debian9'
stretch.vm.network "public_network", bridge: NETWORK_BRIDGE
stretch.vm.provision :shell, :inline => "wget #{SNIPEIT_SH_URL}"
stretch.vm.provision :shell, :inline => "chmod 755 snipeit.sh"
end
config.vm.define "fedora27" do |fedora27|
fedora27.vm.box = "fedora/27-cloud-base"
fedora27.vm.hostname = 'fedora27'
fedora27.vm.network "public_network", bridge: NETWORK_BRIDGE
fedora27.vm.provision :shell, :inline => "dnf -y install wget"
fedora27.vm.provision :shell, :inline => "wget #{SNIPEIT_SH_URL}"
fedora27.vm.provision :shell, :inline => "chmod 755 snipeit.sh"
end
config.vm.define "fedora26" do |fedora26|
fedora26.vm.box = "fedora/26-cloud-base"
fedora26.vm.hostname = 'fedora26'
fedora26.vm.network "public_network", bridge: NETWORK_BRIDGE
fedora26.vm.provision :shell, :inline => "dnf -y install wget"
fedora26.vm.provision :shell, :inline => "wget #{SNIPEIT_SH_URL}"
fedora26.vm.provision :shell, :inline => "chmod 755 snipeit.sh"
end
config.vm.define "freebsd" do |freebsd|
freebsd.vm.box = "freebsd/FreeBSD-11.2-RELEASE"
freebsd.vm.hostname = 'freebsd12'
freebsd.vm.network "forwarded_port", guest: 80, host: 8080
freebsd.vm.network "forwarded_port", guest:3306, host:3306 # mysql
freebsd.vm.network "private_network", type: "dhcp"
freebsd.ssh.shell = "sh"
freebsd.vm.base_mac = "080027D14C66"
freebsd.vm.synced_folder ".", "/vagrant", :nfs => true, id: "vagrant-root",
:mount_options => ['rw', 'vers=3', 'tcp', 'actimeo=2']
freebsd.vm.provision "shell", inline: <<-SHELL
pkg install -y python27;
SHELL
freebsd.vm.provision "ansible" do |ansible|
ansible.playbook = "ansible/freebsd/vagrant_playbook.yml"
end
end
end
-260
View File
@@ -1,260 +0,0 @@
---
- name: Set up local server
hosts: all
remote_user: vagrant
become_user: root
become_method: sudo
vars:
- ansible_python_interpreter: /usr/local/bin/python2.7
gather_facts: no
# Tasks
tasks:
#
# Update the PKG database
#
- name: Upgrade PKG database
raw: sudo pkg upgrade -y
#
# Mount the shared folders
#
- name: Update Vagrant Shared Folders
command: "{{ item }}"
with_items:
- sysrc rpc_lockd_enable=YES
- sysrc rpc_statd_enable=YES
become: true
#
# Install required utilities
#
- name: Install Utilities
pkgng:
name: "{{ item }}"
state: present
with_items:
- openssl
- node
- npm
- git
- nano
- wget
- bash
become: true
#
# Install php and php dependancies
#
- name: Install PHP dependancies
pkgng:
name: "{{ item }}"
state: present
with_items:
- php72
- php72-zip
- php72-zlib
- php72-extensions
- php72-mbstring
- php72-openssl
# - php72-mysqli
- php72-curl
- php72-soap
- php72-pdo_mysql
# - php72-pdo_pgsql
- php72-ldap
- php72-curl
- php72-fileinfo
- php72-bcmath
- php72-gd
become: true
#
# Create a php.ini file
#
- name: PHP INI check
stat:
path: /usr/local/etc/php.ini
register: php_ini_exits
- name: Create PHP ini
command: cp /usr/local/etc/php.ini-development /usr/local/etc/php.ini
become: true
when: not php_ini_exits.stat.exists
- name: Enable PHP-FPM auto-start
command: sysrc php_fpm_enable=YES
become: true
- name: Start PHP-FPM service
service:
name: php-fpm
state: started
become: true
#
# Install the lastest version of composer
#
- name: Composer check
stat:
path: /usr/local/bin/composer
register: composer_exits
- name: Install Composer
shell: |
EXPECTED_SIGNATURE=$(wget -q -O - https://composer.github.io/installer.sig)
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_SIGNATURE=$(php -r "echo hash_file('SHA384', 'composer-setup.php');")
if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
then
>&2 echo 'ERROR: Invalid installer signature'
rm composer-setup.php
exit 1
fi
php composer-setup.php --quiet
RESULT=$?
rm composer-setup.php
mv composer.phar /usr/local/bin/composer
exit $RESULT
when: not composer_exits.stat.exists
become: true
#
# Install MySQL Server
- name: Install MySQL 5.7
pkgng:
name: mysql57-server
state: present
become: true
register: sql_server
- name: Start MySQL server
service:
name: mysql-server
state: started
become: true
- name: MySQL 5.7 auto-start
command: sysrc mysql_enable=YES
become: true
when: sql_server.changed == true
- name: Get MySQL root password
command: tail -1 /root/.mysql_secret
register: myql_root_pwd
become: true
when: sql_server.changed == true
- name: Change MySQL root password
command: mysqladmin -u root -p'{{myql_root_pwd.stdout}}' password vagrant
when: sql_server.changed == true
- name: Enable remote mysql
replace:
path: /usr/local/etc/mysql/my.cnf
regexp: "127.0.0.1"
replace: "0.0.0.0"
become: true
when: sql_server.changed == true
- name: Grant user vagrant privelages
shell: mysql -u root -pvagrant -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'vagrant' WITH GRANT OPTION; FLUSH PRIVILEGES;"
become: true
when: sql_server.changed == true
ignore_errors: true
- name: Restart MySQL server
service:
name: mysql-server
state: restarted
become: true
#
# Install Apache Web Server
#
- name: Install Apache 2.4
pkgng:
name: apache24
state: present
become: true
register: apache24_server
- name: Apache 2.4 auto-start
command: sysrc apache24_enable=YES
become: true
when: apache24_server.changed == true
- name: Enable Apache modules
replace:
path: /usr/local/etc/apache24/httpd.conf
regexp: "#{{ item }}"
replace: "{{ item }}"
become: true
with_items:
- LoadModule rewrite_module libexec/apache24/mod_rewrite.so
- LoadModule vhost_alias_module libexec/apache24/mod_vhost_alias.so
- LoadModule deflate_module libexec/apache24/mod_deflate.so
- LoadModule expires_module libexec/apache24/mod_expires.so
- LoadModule mpm_worker_module libexec/apache24/mod_mpm_worker.so
- LoadModule proxy_fcgi_module libexec/apache24/mod_proxy_fcgi.so
- LoadModule proxy_module libexec/apache24/mod_proxy.so
- Include etc/apache24/extra/httpd-vhosts.conf
when: apache24_server.changed == true
- name: Disable Apache modules
replace:
path: /usr/local/etc/apache24/httpd.conf
regexp: "{{ item }}"
replace: "#{{ item }}"
become: true
with_items:
- LoadModule mpm_prefork_module libexec/apache24/mod_mpm_prefork.so
when: apache24_server.changed == true
- name: Backup vhosts
command: cp /usr/local/etc/apache24/extra/httpd-vhosts.conf /usr/local/etc/apache24/extra/httpd-vhosts.conf.bak
become: true
when: apache24_server.changed == true
- name: Truncate vhosts
command: truncate -s 0 /usr/local/etc/apache24/extra/httpd-vhosts.conf
become: true
when: apache24_server.changed == true
- name: Set up vhost
blockinfile:
path: "/usr/local/etc/apache24/extra/httpd-vhosts.conf"
block: |
<VirtualHost *>
DocumentRoot /usr/local/www/apache24/data/public
ServerName vagrant.app
ProxyPassMatch ^/(.*\.php(/.*)?)$ fcgi://127.0.0.1:9000/usr/local/www/apache24/data/public/$1
DirectoryIndex /index.php index.php
<Directory /usr/local/www/apache24/data/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
become: true
when: apache24_server.changed == true
- name: Map apache dir to local folder
shell: |
if ! [ -L /var/www ]; then
rm -rf /usr/local/www/apache24/data;
ln -fs /vagrant /usr/local/www/apache24/data;
fi
become: true
when: apache24_server.changed == true
- name: Start Apache 2.4 server
service:
name: apache24
state: started
become: true
-10
View File
@@ -1,10 +0,0 @@
<VirtualHost *:80>
<Directory {{ app_path }}/public>
Allow From All
AllowOverride All
Options -Indexes
</Directory>
DocumentRoot {{ app_path }}/public
ServerName {{ fqdn }}
</VirtualHost>
-226
View File
@@ -1,226 +0,0 @@
---
- name: Set up local server
hosts: all
remote_user: vagrant
become_user: root
become_method: sudo
vars:
app_path: "/var/www/snipeit"
fqdn: "localhost"
tasks:
- name: Update and upgrade existing apt packages
become: true
apt:
upgrade: yes
update_cache: yes
- name: Install Utilities
become: true
apt:
name: "{{ packages }}"
state: present
vars:
packages:
- nano
- vim
- name: Installing Apache httpd, PHP, MariaDB and other requirements.
become: true
apt:
name: "{{ packages }}"
state: present
vars:
packages:
- mariadb-client
- php
- php-curl
- php-mysql
- php-gd
- php-ldap
- php-zip
- php-mbstring
- php-xml
- php-bcmath
- curl
- git
- unzip
- python-pymysql
#
# Install the lastest version of composer
#
- name: Composer check
stat:
path: /usr/local/bin/composer
register: composer_exits
- name: Install Composer
shell: |
EXPECTED_SIGNATURE=$(wget -q -O - https://composer.github.io/installer.sig)
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_SIGNATURE=$(php -r "echo hash_file('SHA384', 'composer-setup.php');")
if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
then
>&2 echo 'ERROR: Invalid installer signature'
rm composer-setup.php
exit 1
fi
php composer-setup.php --quiet
RESULT=$?
rm composer-setup.php
mv composer.phar /usr/local/bin/composer
exit $RESULT
when: not composer_exits.stat.exists
args:
creates: /usr/local/bin/composer
become: true
#
# Install and Configure MariaDB
#
- name: Install MariaDB
become: true
apt:
name: mariadb-server
state: present
register: sql_server
- name: Start and Enable MySQL server
become: true
systemd:
state: started
enabled: yes
name: mariadb
- name: Create Vagrant mysql password
become: true
mysql_user:
name: vagrant
password: vagrant
login_unix_socket: /var/run/mysqld/mysqld.sock
priv: "*.*:ALL"
state: present
- name: Enable remote mysql
replace:
path: /etc/mysql/mariadb.conf.d/50-server.cnf
regexp: "127.0.0.1"
replace: "0.0.0.0"
become: true
notify:
- restart mysql
- name: Create snipeit database
become: true
mysql_db:
name: snipeit
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
#
# Install Apache Web Server
#
- name: Install Apache 2.4
apt:
name: "{{ packages }}"
state: present
vars:
packages:
- apache2
- libapache2-mod-php
become: true
register: apache2_server
- name: Start and Enable Apache2 Server
become: true
systemd:
name: apache2
state: started
enabled: yes
#- name: Disable Apache modules
# become: true
# apache2_module:
# state: absent
# name: "{{ item }}"
# with_items:
# #- mpm_prefork
# notify:
# - restart apache2
- name: Enable Apache modules
become: true
apache2_module:
state: present
name: "{{ item }}"
with_items:
- rewrite
- vhost_alias
- deflate
- expires
- proxy_fcgi
- proxy
notify:
- restart apache2
- name: Install Apache VirtualHost File
become: true
template:
src: apachevirtualhost.conf.j2
dest: "/etc/apache2/sites-available/snipeit.conf"
- name: Enable VirtualHost
become: true
command: a2ensite snipeit
args:
creates: /etc/apache2/sites-enabled/snipeit.conf
notify:
- restart apache2
- name: Map apache dir to local folder
become: true
file:
src: /vagrant
dest: "{{ app_path }}"
state: link
notify:
- restart apache2
#
# Install dependencies from composer
#
- name: Install dependencies from composer
composer:
command: install
working_dir: "{{ app_path }}"
notify:
- restart apache2
#
# Configure .env file
#
- name: Copy .env file
copy:
src: "{{ app_path }}/.env.example"
dest: "{{ app_path }}/.env"
- name: Configure .env file
lineinfile:
path: "{{ app_path }}/.env"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
with_items:
- { regexp: '^DB_HOST=', line: 'DB_HOST=127.0.0.1'}
- { regexp: '^DB_DATABASE=', line: 'DB_DATABASE=snipeit' }
- { regexp: '^DB_USERNAME=', line: 'DB_USERNAME=vagrant' }
- { regexp: '^DB_PASSWORD=', line: 'DB_PASSWORD=vagrant' }
- { regexp: '^APP_URL=', line: "APP_URL=http://{{ fqdn }}" }
- { regexp: '^APP_ENV=', line: "APP_ENV=development" }
- { regexp: '^APP_DEBUG=', line: "APP_DEBUG=true" }
- name: Generate application key
shell: "php {{ app_path }}/artisan key:generate --force"
- name: Artisan Migrate
shell: "php {{ app_path }}/artisan migrate --force"
#
# Create Cron Job
#
- name: Create scheduler cron job
become: true
cron:
name: "Snipe-IT Artisan Scheduler"
job: "/usr/bin/php {{ app_path }}/artisan schedule:run"
handlers:
- name: restart apache2
become: true
systemd:
name: apache2
state: restarted
- name: restart mysql
become: true
systemd:
name: mysql
state: restarted
-154
View File
@@ -1,154 +0,0 @@
{
"name": "Snipe-IT",
"description": "Open source asset management.",
"keywords": [
"asset management",
"it asset"
],
"website": "https://snipeitapp.com/",
"repository": "https://github.com/snipe/snipe-it",
"logo": "https://pbs.twimg.com/profile_images/976748875733020672/K-HnZCCK_400x400.jpg",
"success_url": "/setup",
"env": {
"APP_ENV": {
"description": "Laravel environment mode. Unless developing the application, this should be production.",
"value": "production"
},
"APP_DEBUG": {
"description": "Laravel debug mode. Unless developing the application or actively debugging a problem, this should be set to false.",
"value": "false"
},
"APP_KEY": {
"description": "A secret key for verifying the integrity of signed cookies. (See either https://snipe-it.readme.io/docs/generate-your-app-key or generate at https://coderstoolbox.online/toolbox/generate-symfony-secret)",
"value": ""
},
"APP_URL": {
"description": "URL where your Snipe-IT install will be available at.",
"value": "https://your-app-name.herokuapp.com"
},
"APP_TIMEZONE": {
"description": "Which timezone do you want to use for your install? (http://php.net/manual/en/timezones.php)",
"value": "UTC"
},
"APP_LOCALE": {
"description": "Which language do you want to use for your install? (https://snipe-it.readme.io/docs/configuration#setting-a-language)",
"value": "en"
},
"MAX_RESULTS": {
"description": "The maximum number of search results that can be returned at one time.",
"value": "500"
},
"MAIL_MAILER": {
"description": "Mail driver - Generally SMTP on Heroku - https://snipe-it.readme.io/docs/configuration#required-outgoing-mail-settings",
"value": "smtp"
},
"MAIL_HOST": {
"description": "SMTP Server Hostname",
"value": "smtp.your.domain.name"
},
"MAIL_PORT": {
"description": "SMTP Server Port",
"value": "25"
},
"MAIL_USERNAME": {
"description": "SMTP Server Username",
"value": "YOURUSERNAME"
},
"MAIL_PASSWORD": {
"description": "SMTP Server Password",
"value": "YOURPASSWORD"
},
"MAIL_TLS_VERIFY_PEER": {
"description": "Ensure validity of TLS certificate on remote mail server",
"value": true
},
"MAIL_FROM_ADDR": {
"description": "Email from address",
"value": "no-reply@domain.name"
},
"MAIL_FROM_NAME": {
"description": "Email from Name",
"value": "Snipe-IT"
},
"MAIL_REPLYTO_ADDR": {
"description": "Email Reply-To address",
"value": "your@domain.name"
},
"MAIL_REPLYTO_NAME": {
"description": "Email Reply-To Name",
"value": "Snipe-IT"
},
"MAIL_AUTO_EMBED": {
"description": "Whether or not to embed images in emails (via CID or base64) versus linking to them.",
"value": "true"
},
"MAIL_AUTO_EMBED_METHOD": {
"description": "Method that should be used for attaching inline images.",
"value": "base64"
},
"SESSION_LIFETIME": {
"description": "Specify the time in minutes that the session should remain valid.",
"value": "12000"
},
"EXPIRE_ON_CLOSE": {
"description": "Specify whether or not the logged in session should be expired when the user closes their browser window.",
"value": "false"
},
"ENCRYPT": {
"description": "Specify whether you wish to use encrypted cookies for your Snipe-IT sessions.",
"value": "true"
},
"COOKIE_NAME": {
"description": "The name of the cookie set by Snipe-IT for session management.",
"value": "snipeit_session"
},
"COOKIE_DOMAIN": {
"description": "The domain name that the session cookie should be sent for.",
"value": "your-app-name.herokuapp.com"
},
"SECURE_COOKIES": {
"description": "Should cookies only be sent for HTTPS connections? Generally true on Heroku.",
"value": "true"
},
"LOGIN_MAX_ATTEMPTS": {
"description": "The maximum number of failed attempts allowed before the user is throttled.",
"value": "5"
},
"LOGIN_LOCKOUT_DURATION": {
"description": "The duration (in seconds) that the user should be blocked from attempting to authenticate again.",
"value": "60"
},
"LOG_CHANNEL": {
"description": "Driver to send logs to. (errorlog for stderr)",
"value": "errorlog"
},
"ALLOW_IFRAMING": {
"description": "Allow Snipe-IT to be loaded using an iFrame?",
"value": "false"
},
"GOOGLE_MAPS_API": {
"description": "Include your Google Maps API key here if you'd like Snipe-IT to load maps from Google on your locations and suppliers pages.",
"required": false
},
"BACKUP_ENV": {
"description": "Set this to true if you wish to backup your .env file in your Admin > Backups process.",
"value": "true"
},
"ENABLE_HSTS": {
"description": "Whether or not to send the HSTS security policy header.",
"value": "false"
}
},
"formation": {
"web": {
"quantity": 1,
"size": "free"
}
},
"image": "heroku/php",
"addons": [
"cleardb:ignite",
"heroku-redis:mini",
"papertrail:choklad"
]
}
@@ -1,88 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
class CheckinLicensesFromAllUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:checkin-from-all {--license_id=} {--notify}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Checks in licenses from all users';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$license_id = $this->option('license_id');
$notify = $this->option('notify');
if (! $license_id) {
$this->error('ERROR: License ID is required.');
return false;
}
if (! $license = License::where('id', '=', $license_id)->first()) {
$this->error('Invalid license ID');
return false;
}
$this->info('Checking in ALL seats for '.$license->name);
$licenseSeats = LicenseSeat::where('license_id', '=', $license_id)
->whereNotNull('assigned_to')
->with('user')
->get();
$this->info(' There are '.$licenseSeats->count().' seats checked out: ');
if (! $notify) {
$this->info('No mail will be sent.');
}
foreach ($licenseSeats as $seat) {
$this->info($seat->user->username.' has a license seat for '.$license->name);
$seat->assigned_to = null;
if ($seat->save()) {
// Override the email address so we don't notify on checkin
if (! $notify) {
$seat->user->email = null;
}
// Log the checkin
$seat->logCheckin($seat->user, 'Checked in via cli tool');
}
}
}
}
@@ -1,107 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
class CheckoutLicenseToAllUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:checkout-to-all {--license_id=} {--notify}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Checks out licenses to all users';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$license_id = $this->option('license_id');
$notify = $this->option('notify');
if (! $license_id) {
$this->error('ERROR: License ID is required.');
return false;
}
if (! $license = License::where('id', '=', $license_id)->with('assignedusers')->first()) {
$this->error('Invalid license ID');
return false;
}
$users = User::whereNull('deleted_at')->where('autoassign_licenses', '=', 1)->with('licenses')->get();
if ($users->count() > $license->getAvailSeatsCountAttribute()) {
$this->info('You do not have enough free seats to complete this task, so we will check out as many as we can. ');
}
$this->info('Checking out '.$users->count().' of '.$license->getAvailSeatsCountAttribute().' seats for '.$license->name);
if (! $notify) {
$this->info('No mail will be sent.');
}
foreach ($users as $user) {
// Check to make sure this user doesn't already have this license checked out
// to them
if ($user->licenses->where('id', '=', $license_id)->count()) {
$this->info($user->username.' already has this license checked out to them. Skipping... ');
continue;
}
// If the license is valid, check that there is an available seat
if ($license->availCount()->count() < 1) {
$this->error('ERROR: No available seats');
return false;
}
$this->info($license->availCount()->count().' seats left');
// Get the seat ID
$licenseSeat = $license->freeSeat();
// Update the seat with checkout info,
$licenseSeat->assigned_to = $user->id;
if ($licenseSeat->save()) {
// Temporarily null the user's email address so we don't send mail if we're not supposed to
if (! $notify) {
$user->email = null;
}
// Log the checkout
$licenseSeat->logCheckout('Checked out via cli tool', $user);
$this->info('License '.$license_id.' seat '.$licenseSeat->id.' checked out to '.$user->username);
}
}
}
}
-95
View File
@@ -1,95 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use \App\Models\User;
class CreateAdmin extends Command
{
/** @mixin User **/
/**
* App\Console\CreateAdmin
* @property mixed $first_name
* @property string $last_name
* @property string $username
* @property string $email
* @property string $permissions
* @property string $password
* @property boolean $activated
* @property boolean $show_in_list
* @property boolean $autoassign_licenses
* @property \Illuminate\Support\Carbon|null $created_at
* @property mixed $created_by
*/
protected $signature = 'snipeit:create-admin {--first_name=} {--last_name=} {--email=} {--username=} {--password=} {show_in_list?} {autoassign_licenses?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create an admin user via command line.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
public function handle()
{
$first_name = $this->option('first_name');
$last_name = $this->option('last_name');
$username = $this->option('username');
$email = $this->option('email');
$password = $this->option('password');
$show_in_list = $this->argument('show_in_list');
$autoassign_licenses = $this->argument('autoassign_licenses');
if (($first_name == '') || ($last_name == '') || ($username == '') || ($email == '') || ($password == '')) {
$this->info('ERROR: All fields are required.');
} else {
$user = new User;
$user->first_name = $first_name;
$user->last_name = $last_name;
$user->username = $username;
$user->email = $email;
$user->permissions = '{"admin":1,"user":1,"superuser":1,"reports.view":1, "licenses.keys":1}';
$user->password = bcrypt($password);
$user->activated = 1;
if ($show_in_list == 'false') {
$user->show_in_list = 0;
}
if ($autoassign_licenses == 'false') {
$user->autoassign_licenses = 0;
}
if ($user->save()) {
$this->info('New user created');
$user->groups()->attach(1);
} else {
$this->info('Admin user was not created');
$errors = $user->getErrors();
foreach ($errors->all() as $error) {
$this->info('ERROR:'.$error);
}
}
}
}
}
-53
View File
@@ -1,53 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Setting;
use Illuminate\Console\Command;
class DisableLDAP extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:ldap-disable';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This is a rescue command that can be used to turn off LDAP settings in the event that you managed to lock yourself out using bad LDAP settings.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->confirm("\n****************************************************\nThis will disable LDAP support. You will not be able \nto login with an account that does not exist \nlocally in the Snipe-IT local database. \n****************************************************\n\nDo you wish to continue? [y|N]")) {
$setting = Setting::getSettings();
$setting->ldap_enabled = 0;
if ($setting->save()) {
$this->info('LDAP has been set to disabled.');
} else {
$this->info('Unable to disable LDAP.');
}
} else {
$this->info('Canceled. No actions taken.');
}
}
}
-79
View File
@@ -1,79 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class FixDoubleEscape extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:unescape';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This should be run to fix some double-escaping issues from earlier versions of Snipe-IT.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$tables = [
\App\Models\Asset::class => ['name'],
\App\Models\License::class => ['name'],
\App\Models\Consumable::class => ['name'],
\App\Models\Accessory::class => ['name'],
\App\Models\Component::class => ['name'],
\App\Models\Company::class => ['name'],
\App\Models\Manufacturer::class => ['name'],
\App\Models\Supplier::class => ['name'],
\App\Models\Statuslabel::class => ['name'],
\App\Models\Depreciation::class => ['name'],
\App\Models\AssetModel::class => ['name'],
\App\Models\Group::class => ['name'],
\App\Models\Department::class => ['name'],
\App\Models\Location::class => ['name'],
\App\Models\User::class => ['first_name', 'last_name'],
];
$count = [];
foreach ($tables as $classname => $fields) {
$count[$classname] = [];
$count[$classname]['classname'] = 0;
foreach ($fields as $field) {
$count[$classname]['classname']++;
$count[$classname][$field] = 0;
foreach ($classname::where("$field", 'LIKE', '%&%')->get() as $row) {
$this->info('Updating '.$field.' for '.$classname);
$row->{$field} = html_entity_decode($row->{$field}, ENT_QUOTES);
$row->save();
$count[$classname][$field]++;
}
}
}
$this->info('Update complete');
}
}
@@ -1,99 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Console\Command;
class FixMismatchedAssetsAndLogs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:fix-assets-and-logs {--dryrun : Run the sync process but don\'t update the database}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This script attempts to check the log table and check that the assets.assigned_to matches the last checkout.';
/**
* Is dry-run?
*
* @var bool
*/
private $dryrun = false;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->option('dryrun')) {
$this->dryrun = true;
}
if ($this->dryrun) {
$this->info('This is a DRY RUN - no changes will be saved.');
}
$mismatch_count = 0;
$assets = Asset::whereNotNull('assigned_to')
->where('assigned_type', '=', \App\Models\User::class)
->orderBy('id', 'ASC')->get();
foreach ($assets as $asset) {
// get the last checkout of the asset
if ($checkout_log = Actionlog::where('target_type', '=', \App\Models\User::class)
->where('action_type', '=', 'checkout')
->where('item_id', '=', $asset->id)
->orderBy('created_at', 'DESC')
->first()) {
// Now check for a subsequent checkin log - we want to ignore those
if (! $checkin_log = Actionlog::where('target_type', '=', \App\Models\User::class)
->where('action_type', '=', 'checkin from')
->where('item_id', '=', $asset->id)
->whereDate('created_at', '>', $checkout_log->created_at)
->orderBy('created_at', 'DESC')
->first()) {
//print_r($asset);
if ($checkout_log->target_id != $asset->assigned_to) {
$this->error('Log ID: '.$checkout_log->id.' -- Asset ID '.$checkout_log->item_id.' SHOULD BE checked out to User '.$checkout_log->target_id.' but its assigned_to is '.$asset->assigned_to);
if (! $this->dryrun) {
$asset->assigned_to = $checkout_log->target_id;
if ($asset->save()) {
$this->info('Asset record updated.');
} else {
$this->error('Error updating asset: '.$asset->getErrors());
}
}
$mismatch_count++;
}
} else {
//$this->info('Asset ID '.$asset->id.': There is a checkin '.$checkin_log->created_at.' after this checkout '.$checkout_log->created_at);
}
}
}
$this->info($mismatch_count.' mismatched assets.');
}
}
@@ -1,97 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Helpers\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Laravel\Passport\TokenRepository;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
use Illuminate\Support\Facades\DB;
class GeneratePersonalAccessToken extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:make-api-key
{--user_id= : The ID of the user to create the token for.}
{--name= : The name of the new API token}
{--key-only : Only return the value of the API key}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This console command allows you to generate Personal API tokens to be used with the Snipe-IT JSON REST API on behalf of a user.';
/**
* The token repository implementation.
*
* @var \Laravel\Passport\TokenRepository
*/
protected $tokenRepository;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(TokenRepository $tokenRepository, ValidationFactory $validation)
{
$this->validation = $validation;
$this->tokenRepository = $tokenRepository;
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$accessTokenName = $this->option('name');
if ($accessTokenName=='') {
$accessTokenName = 'CLI Auth Token';
}
if ($this->option('user_id')=='') {
return $this->error('ERROR: user_id cannot be blank.');
}
if ($user = User::find($this->option('user_id'))) {
$createAccessToken = $user->createToken($accessTokenName)->accessToken;
if ($this->option('key-only')) {
$this->info($createAccessToken);
} else {
$this->warn('Your API Token has been created. Be sure to copy this token now, as it will not be accessible again.');
if ($token = DB::table('oauth_access_tokens')->where('user_id', '=', $user->id)->where('name','=',$accessTokenName)->orderBy('created_at', 'desc')->first()) {
$this->info('API Token ID: '.$token->id);
}
$this->info('API Token User: '.$user->present()->fullName.' ('.$user->username.')');
$this->info('API Token Name: '.$accessTokenName);
$this->info('API Token: '.$createAccessToken);
}
} else {
return $this->error('ERROR: Invalid user. API key was not created.');
}
}
}
-150
View File
@@ -1,150 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Location;
use Illuminate\Console\Command;
use League\Csv\Reader;
class ImportLocations extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:import-locations {filename}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import locations and their parents';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (! ini_get('auto_detect_line_endings')) {
ini_set('auto_detect_line_endings', '1');
}
$filename = $this->argument('filename');
$csv = Reader::createFromPath(storage_path('private_uploads/imports/').$filename, 'r');
$this->info('Attempting to process: '.storage_path('private_uploads/imports/').$filename);
$csv->setHeaderOffset(0); //because we don't want to insert the header
$results = $csv->getRecords();
// Import parent location names first if they don't exist
foreach ($results as $parent_index => $parent_row) {
if (array_key_exists('Parent Name', $parent_row)) {
$parent_name = trim($parent_row['Parent Name']);
if (array_key_exists('Name', $parent_row)) {
$this->info('- Parent: '.$parent_name.' in row as: '.trim($parent_row['Parent Name']));
}
// Save parent location name
// This creates a sort of name-stub that we'll update later on in this script
$parent_location = Location::firstOrCreate(['name' => $parent_name]);
if (array_key_exists('Name', $parent_row)) {
$this->info('Parent for '.$parent_row['Name'].' is '.$parent_name.'. Attempting to save '.$parent_name.'.');
}
// Check if the record was updated or created.
// This is mostly for clearer debugging.
if ($parent_location->exists) {
$this->info('- Parent location '.$parent_name.' already exists.');
} else {
$this->info('- Parent location '.$parent_name.' was created.');
}
} else {
$this->info('- No Parent Name provided, so no parent location will be created.');
}
}
$this->info('----- Parents Created.... backfilling additional details... --------');
// Loop through ALL records and add/update them if there are additional fields
// besides name
foreach ($results as $index => $row) {
if (array_key_exists('Parent Name', $row)) {
$parent_name = trim($row['Parent Name']);
} else {
$parent_name = null;
}
// Set the location attributes to save
if (array_key_exists('Name', $row)) {
$location = Location::firstOrCreate(['name' => trim($row['Name'])]);
$location->name = trim($row['Name']);
$this->info('Checking location: '.$location->name);
} else {
$this->error('Location name is required and is missing from at least one row in this dataset. Check your CSV for extra trailing rows and try again.');
return false;
}
if (array_key_exists('Currency', $row)) {
$location->currency = trim($row['Currency']);
}
if (array_key_exists('Address 1', $row)) {
$location->address = trim($row['Address 1']);
}
if (array_key_exists('Address 2', $row)) {
$location->address2 = trim($row['Address 2']);
}
if (array_key_exists('City', $row)) {
$location->city = trim($row['City']);
}
if (array_key_exists('State', $row)) {
$location->state = trim($row['State']);
}
if (array_key_exists('Zip', $row)) {
$location->zip = trim($row['Zip']);
}
if (array_key_exists('Country', $row)) {
$location->country = trim($row['Country']);
}
if (array_key_exists('OU', $row)) {
$location->ldap_ou = trim($row['OU']);
}
// If a parent name is provided, we created it earlier in the script,
// so let's grab that ID
if ($parent_name) {
$this->info('-- Searching for Parent Name: '.$parent_name);
$parent = Location::where('name', '=', $parent_name)->first();
$location->parent_id = $parent->id;
$this->info('Parent: '.$parent_name.' - ID: '.$parent->id);
}
// Make sure the more advanced (non-name) fields pass validation
if (($location->isValid()) && ($location->save())) {
// Check if the record was updated or created.
// This is mostly for clearer debugging.
if ($location->exists) {
$this->info('Location '.$location->name.' already exists. Updating...');
} else {
$this->info('- Location '.$location->name.' was created. ');
}
// If there's a validation error, display that
} else {
$this->error('- Non-parent Location '.$location->name.' could not be created: '.$location->getErrors());
}
}
}
}
-59
View File
@@ -1,59 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class KillAllSessions extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:global-logout {--force : Skip the danger prompt; assuming you enter "y"} ';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command will destroy all web sessions on disk and will force a re-login for all users.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!$this->option('force') && !$this->confirm("****************************************************\nTHIS WILL FORCE A LOGIN FOR ALL LOGGED IN USERS.\n\nAre you SURE you wish to continue? ")) {
return $this->error("Session loss not confirmed");
}
$session_files = glob(storage_path("framework/sessions/*"));
$count = 0;
foreach ($session_files as $file) {
if (is_file($file))
unlink($file);
$count++;
}
\DB::table('users')->update(['remember_token' => null]);
$this->info($count. ' sessions cleared!');
}
}
-431
View File
@@ -1,431 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Department;
use App\Models\Group;
use Illuminate\Console\Command;
use App\Models\Setting;
use App\Models\Ldap;
use App\Models\User;
use App\Models\Location;
use Illuminate\Support\Facades\Log;
class LdapSync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:ldap-sync {--location=} {--location_id=*} {--base_dn=} {--filter=} {--summary} {--json_summary}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command line LDAP sync';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// If LDAP enabled isn't set to 1 (ldap_enabled!=1) then we should cut this short immediately without going any further
if (Setting::getSettings()->ldap_enabled!='1') {
$this->error('LDAP is not enabled. Aborting. See Settings > LDAP to enable it.');
exit();
}
ini_set('max_execution_time', env('LDAP_TIME_LIM', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('LDAP_MEM_LIM', '500M'));
$ldap_result_username = Setting::getSettings()->ldap_username_field;
$ldap_result_last_name = Setting::getSettings()->ldap_lname_field;
$ldap_result_first_name = Setting::getSettings()->ldap_fname_field;
$ldap_result_active_flag = Setting::getSettings()->ldap_active_flag;
$ldap_result_emp_num = Setting::getSettings()->ldap_emp_num;
$ldap_result_email = Setting::getSettings()->ldap_email;
$ldap_result_phone = Setting::getSettings()->ldap_phone_field;
$ldap_result_jobtitle = Setting::getSettings()->ldap_jobtitle;
$ldap_result_country = Setting::getSettings()->ldap_country;
$ldap_result_location = Setting::getSettings()->ldap_location;
$ldap_result_dept = Setting::getSettings()->ldap_dept;
$ldap_result_manager = Setting::getSettings()->ldap_manager;
$ldap_default_group = Setting::getSettings()->ldap_default_group;
$search_base = Setting::getSettings()->ldap_base_dn;
try {
$ldapconn = Ldap::connectToLdap();
Ldap::bindAdminToLdap($ldapconn);
} catch (\Exception $e) {
if ($this->option('json_summary')) {
$json_summary = ['error' => true, 'error_message' => $e->getMessage(), 'summary' => []];
$this->info(json_encode($json_summary));
}
Log::info($e);
return [];
}
$summary = [];
try {
/**
* if a location ID has been specified, use that OU
*/
if ( $this->option('location_id') ) {
foreach($this->option('location_id') as $location_id){
$location_ou = Location::where('id', '=', $location_id)->value('ldap_ou');
$search_base = $location_ou;
Log::debug('Importing users from specified location OU: \"'.$search_base.'\".');
}
}
/**
* if a manual base DN has been specified, use that. Allow the Base DN to override
* even if there's a location-based DN - if you picked it, you must have picked it for a reason.
*/
if ($this->option('base_dn') != '') {
$search_base = $this->option('base_dn');
Log::debug('Importing users from specified base DN: \"'.$search_base.'\".');
}
/**
* If a filter has been specified, use that
*/
if ($this->option('filter') != '') {
$results = Ldap::findLdapUsers($search_base, -1, $this->option('filter'));
} else {
$results = Ldap::findLdapUsers($search_base);
}
} catch (\Exception $e) {
if ($this->option('json_summary')) {
$json_summary = ['error' => true, 'error_message' => $e->getMessage(), 'summary' => []];
$this->info(json_encode($json_summary));
}
Log::info($e);
return [];
}
/* Determine which location to assign users to by default. */
$location = null; // TODO - this would be better called "$default_location", which is more explicit about its purpose
if ($this->option('location') != '') {
if ($location = Location::where('name', '=', $this->option('location'))->first()) {
Log::debug('Location name ' . $this->option('location') . ' passed');
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')');
}
} elseif ($this->option('location_id')) {
foreach($this->option('location_id') as $location_id) {
if ($location = Location::where('id', '=', $location_id)->first()) {
Log::debug('Location ID ' . $location_id . ' passed');
Log::debug('Importing to ' . $location->name . ' (' . $location->id . ')');
}
}
}
if (! isset($location)) {
Log::debug('That location is invalid or a location was not provided, so no location will be assigned by default.');
}
/* Process locations with explicitly defined OUs, if doing a full import. */
if ($this->option('base_dn') == '' && $this->option('filter') == '') {
// Retrieve locations with a mapped OU, and sort them from the shallowest to deepest OU (see #3993)
$ldap_ou_locations = Location::where('ldap_ou', '!=', '')->get()->toArray();
$ldap_ou_lengths = [];
foreach ($ldap_ou_locations as $ou_loc) {
$ldap_ou_lengths[] = strlen($ou_loc['ldap_ou']);
}
array_multisort($ldap_ou_lengths, SORT_ASC, $ldap_ou_locations);
if (count($ldap_ou_locations) > 0) {
Log::debug('Some locations have special OUs set. Locations will be automatically set for users in those OUs.');
}
// Inject location information fields
for ($i = 0; $i < $results['count']; $i++) {
$results[$i]['ldap_location_override'] = false;
$results[$i]['location_id'] = 0;
}
// Grab subsets based on location-specific DNs, and overwrite location for these users.
foreach ($ldap_ou_locations as $ldap_loc) {
try {
$location_users = Ldap::findLdapUsers($ldap_loc['ldap_ou']);
} catch (\Exception $e) { // TODO: this is stolen from line 77 or so above
if ($this->option('json_summary')) {
$json_summary = ['error' => true, 'error_message' => trans('admin/users/message.error.ldap_could_not_search').' Location: '.$ldap_loc['name'].' (ID: '.$ldap_loc['id'].') cannot connect to "'.$ldap_loc['ldap_ou'].'" - '.$e->getMessage(), 'summary' => []];
$this->info(json_encode($json_summary));
}
Log::info($e);
return [];
}
$usernames = [];
for ($i = 0; $i < $location_users['count']; $i++) {
if (array_key_exists($ldap_result_username, $location_users[$i])) {
$location_users[$i]['ldap_location_override'] = true;
$location_users[$i]['location_id'] = $ldap_loc['id'];
$usernames[] = $location_users[$i][$ldap_result_username][0];
}
}
// Delete located users from the general group.
foreach ($results as $key => $generic_entry) {
if ((is_array($generic_entry)) && (array_key_exists($ldap_result_username, $generic_entry))) {
if (in_array($generic_entry[$ldap_result_username][0], $usernames)) {
unset($results[$key]);
}
}
}
$global_count = $results['count'];
$results = array_merge($location_users, $results);
$results['count'] = $global_count;
}
}
$manager_cache = [];
if($ldap_default_group != null) {
$default = Group::find($ldap_default_group);
if (!$default) {
$ldap_default_group = null; // un-set the default group if that group doesn't exist
}
}
for ($i = 0; $i < $results['count']; $i++) {
$item = [];
$item['username'] = $results[$i][$ldap_result_username][0] ?? '';
$item['employee_number'] = $results[$i][$ldap_result_emp_num][0] ?? '';
$item['lastname'] = $results[$i][$ldap_result_last_name][0] ?? '';
$item['firstname'] = $results[$i][$ldap_result_first_name][0] ?? '';
$item['email'] = $results[$i][$ldap_result_email][0] ?? '';
$item['ldap_location_override'] = $results[$i]['ldap_location_override'] ?? '';
$item['location_id'] = $results[$i]['location_id'] ?? '';
$item['telephone'] = $results[$i][$ldap_result_phone][0] ?? '';
$item['jobtitle'] = $results[$i][$ldap_result_jobtitle][0] ?? '';
$item['country'] = $results[$i][$ldap_result_country][0] ?? '';
$item['department'] = $results[$i][$ldap_result_dept][0] ?? '';
$item['manager'] = $results[$i][$ldap_result_manager][0] ?? '';
$item['location'] = $results[$i][$ldap_result_location][0] ?? '';
// ONLY if you are using the "ldap_location" option *AND* you have an actual result
if ($ldap_result_location && $item['location']) {
$location = Location::firstOrCreate([
'name' => $item['location'],
]);
}
$department = Department::firstOrCreate([
'name' => $item['department'],
]);
$user = User::where('username', $item['username'])->first();
if ($user) {
// Updating an existing user.
$item['createorupdate'] = 'updated';
} else {
// Creating a new user.
$user = new User;
$user->password = $user->noPassword();
$user->activated = 1; // newly created users can log in by default, unless AD's UAC is in use, or an active flag is set (below)
$item['createorupdate'] = 'created';
}
//If a sync option is not filled in on the LDAP settings don't populate the user field
if($ldap_result_username != null){
$user->username = $item['username'];
}
if($ldap_result_last_name != null){
$user->last_name = $item['lastname'];
}
if($ldap_result_first_name != null){
$user->first_name = $item['firstname'];
}
if($ldap_result_emp_num != null){
$user->employee_num = e($item['employee_number']);
}
if($ldap_result_email != null){
$user->email = $item['email'];
}
if($ldap_result_phone != null){
$user->phone = $item['telephone'];
}
if($ldap_result_jobtitle != null){
$user->jobtitle = $item['jobtitle'];
}
if($ldap_result_country != null){
$user->country = $item['country'];
}
if($ldap_result_dept != null){
$user->department_id = $department->id;
}
if($ldap_result_location != null){
$user->location_id = $location ? $location->id : null;
}
if($ldap_result_manager != null){
if($item['manager'] != null) {
// Check Cache first
if (isset($manager_cache[$item['manager']])) {
// found in cache; use that and avoid extra lookups
$user->manager_id = $manager_cache[$item['manager']];
} else {
// Get the LDAP Manager
try {
$ldap_manager = Ldap::findLdapUsers($item['manager'], -1, $this->option('filter'));
} catch (\Exception $e) {
Log::warning("Manager lookup caused an exception: " . $e->getMessage() . ". Falling back to direct username lookup");
// Hail-mary for Okta manager 'shortnames' - will only work if
// Okta configuration is using full email-address-style usernames
$ldap_manager = [
"count" => 1,
0 => [
$ldap_result_username => [$item['manager']]
]
];
}
if ($ldap_manager["count"] > 0) {
// Get the Manager's username
// PHP LDAP returns every LDAP attribute as an array, and 90% of the time it's an array of just one item. But, hey, it's an array.
$ldapManagerUsername = $ldap_manager[0][$ldap_result_username][0];
// Get User from Manager username.
$ldap_manager = User::where('username', $ldapManagerUsername)->first();
if ($ldap_manager && isset($ldap_manager->id)) {
// Link user to manager id.
$user->manager_id = $ldap_manager->id;
}
}
$manager_cache[$item['manager']] = $ldap_manager && isset($ldap_manager->id) ? $ldap_manager->id : null; // Store results in cache, even if 'failed'
}
}
}
// Sync activated state for Active Directory.
if ( !empty($ldap_result_active_flag)) { // IF we have an 'active' flag set....
// ....then *most* things that are truthy will activate the user. Anything falsey will deactivate them.
// (Specifically, we don't handle a value of '0.0' correctly)
$raw_value = @$results[$i][$ldap_result_active_flag][0];
$filter_var = filter_var($raw_value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
$boolean_cast = (bool)$raw_value;
$user->activated = $filter_var ?? $boolean_cast; // if filter_var() was true or false, use that. If it's null, use the $boolean_cast
} elseif (array_key_exists('useraccountcontrol', $results[$i]) ) {
// ....otherwise, (ie if no 'active' LDAP flag is defined), IF the UAC setting exists,
// ....then use the UAC setting on the account to determine can-log-in vs. cannot-log-in
/* The following is _probably_ the correct logic, but we can't use it because
some users may have been dependent upon the previous behavior, and this
could cause additional access to be available to users they don't want
to allow to log in.
$useraccountcontrol = $results[$i]['useraccountcontrol'][0];
if(
// based on MS docs at: https://support.microsoft.com/en-us/help/305144/how-to-use-useraccountcontrol-to-manipulate-user-account-properties
($useraccountcontrol & 0x200) && // is a NORMAL_ACCOUNT
!($useraccountcontrol & 0x02) && // *and* _not_ ACCOUNTDISABLE
!($useraccountcontrol & 0x10) // *and* _not_ LOCKOUT
) {
$user->activated = 1;
} else {
$user->activated = 0;
} */
$enabled_accounts = [
'512', // 0x200 NORMAL_ACCOUNT
'544', // 0x220 NORMAL_ACCOUNT, PASSWD_NOTREQD
'66048', // 0x10200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD
'66080', // 0x10220 NORMAL_ACCOUNT, PASSWD_NOTREQD, DONT_EXPIRE_PASSWORD
'262656', // 0x40200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED
'262688', // 0x40220 NORMAL_ACCOUNT, PASSWD_NOTREQD, SMARTCARD_REQUIRED
'328192', // 0x50200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
'328224', // 0x50220 NORMAL_ACCOUNT, PASSWD_NOT_REQD, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
'4194816',// 0x400200 NORMAL_ACCOUNT, DONT_REQ_PREAUTH
'4260352', // 0x410200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, DONT_REQ_PREAUTH
'1049088', // 0x100200 NORMAL_ACCOUNT, NOT_DELEGATED
'1114624', // 0x110200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, NOT_DELEGATED,
];
$user->activated = (in_array($results[$i]['useraccountcontrol'][0], $enabled_accounts)) ? 1 : 0;
// If we're not using AD, and there isn't an activated flag set, activate all users
} /* implied 'else' here - leave the $user->activated flag alone. Newly-created accounts will be active.
already-existing accounts will be however the administrator has set them */
if ($item['ldap_location_override'] == true) {
$user->location_id = $item['location_id'];
} elseif ((isset($location)) && (! empty($location))) {
if ((is_array($location)) && (array_key_exists('id', $location))) {
$user->location_id = $location['id'];
} elseif (is_object($location)) {
$user->location_id = $location->id;
}
}
$location = null;
$user->ldap_import = 1;
$errors = '';
if ($user->save()) {
$item['note'] = $item['createorupdate'];
$item['status'] = 'success';
if ( $item['createorupdate'] === 'created' && $ldap_default_group) {
$user->groups()->attach($ldap_default_group);
}
} else {
foreach ($user->getErrors()->getMessages() as $key => $err) {
$errors .= $err[0];
}
$item['note'] = $errors;
$item['status'] = 'error';
}
array_push($summary, $item);
}
if ($this->option('summary')) {
for ($x = 0; $x < count($summary); $x++) {
if ($summary[$x]['status'] == 'error') {
$this->error('ERROR: '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was not imported: '.$summary[$x]['note']);
} else {
$this->info('User '.$summary[$x]['firstname'].' '.$summary[$x]['lastname'].' (username: '.$summary[$x]['username'].') was '.strtoupper($summary[$x]['createorupdate']).'.');
}
}
} elseif ($this->option('json_summary')) {
$json_summary = ['error' => false, 'error_message' => '', 'summary' => $summary]; // hardcoding the error to false and the error_message to blank seems a bit weird
$this->info(json_encode($json_summary));
} else {
return $summary;
}
}
}
-517
View File
@@ -1,517 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Setting;
use Exception;
use Illuminate\Support\Facades\Crypt;
/**
* Check if a given ip is in a network
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
*/
function ip_in_range( $ip, $range ) {
if ( strpos( $range, '/' ) == false ) {
$range .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $range, $netmask ) = explode( '/', $range, 2 );
$range_decimal = ip2long( $range );
$ip_decimal = ip2long( $ip );
$wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
// NOTE - this function was shamelessly stolen from this gist: https://gist.github.com/tott/7684443
/**
* Ensure LDAP filters are parentheses-wrapped
*/
function parenthesized_filter($filter)
{
if(substr($filter,0,1) == "(" ) {
return $filter;
} else {
return "(".$filter.")";
}
}
class LdapTroubleshooter extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ldap:troubleshoot
{--ldap-search : Output an ldapsearch command-line for testing your LDAP config}
{--force : Skip the interactive yes/no prompt for confirmation}
{--debug : Include debugging output (verbose)}
{--trace : Include extremely verbose LDAP trace output}
{--timeout=15 : Timeout for LDAP Bind operations}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Runs a series of non-destructive LDAP commands to help try and determine correct LDAP settings for your environment.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Output something *only* if debug is enabled
*
* @return void
*/
public function debugout($string)
{
if($this->option('debug')) {
$this->line($string);
}
}
/**
* Clean the results from ldap_get_entries into something useful
* @param array $array
* @return array
*/
public function ldap_results_cleaner ($array) {
$cleaned = [];
for($i = 0; $i < $array['count']; $i++) {
$row = $array[$i];
$clean_row = [];
foreach($row AS $key => $val ) {
$this->debugout("Key is: ".$key);
if($key == "count" || is_int($key) || $key == "dn") {
$this->debugout(" and we're gonna skip it\n");
continue;
}
$this->debugout(" And that seems fine.\n");
if(array_key_exists('count',$val)) {
if($val['count'] == 1) {
$clean_row[$key] = $val[0];
} else {
unset($val['count']); //these counts are annoying
$elements = [];
foreach($val as $entry) {
if(isset($ldap_constants[$entry])) {
$elements[] = $ldap_constants[$entry];
} else {
$elements[] = $entry;
}
}
$clean_row[$key] = $elements;
}
} else {
$clean_row[$key] = $val;
}
}
$cleaned[$i] = $clean_row;
}
return $cleaned;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if($this->option('trace')) {
ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 7);
}
$settings = Setting::getSettings();
$this->settings = $settings;
if($this->option('ldap-search')) {
if(!$this->option('force')) {
$confirmation = $this->confirm('WARNING: This command will display your LDAP password on your terminal. Are you sure this is ok?');
if(!$confirmation) {
$this->error('ABORTING');
exit(-1);
}
}
$output = [];
if($settings->ldap_server_cert_ignore) {
$this->line("# Ignoring server certificate validity");
$output[] = "LDAPTLS_REQCERT=never";
}
if($settings->ldap_client_tls_cert && $settings->ldap_client_tls_key) {
$this->line("# Adding LDAP Client Certificate and Key");
$output[] = "LDAPTLS_CERT=storage/ldap_client_tls.cert";
$output[] = "LDAPTLS_KEY=storage/ldap_client_tls.key";
}
$output[] = "ldapsearch";
$output[] = "-H ".$settings->ldap_server;
$output[] = "-x";
$output[] = "-b ".escapeshellarg($settings->ldap_basedn);
$output[] = "-D ".escapeshellarg($settings->ldap_uname);
$output[] = "-w ".escapeshellarg(Crypt::Decrypt($settings->ldap_pword));
$output[] = escapeshellarg(parenthesized_filter($settings->ldap_filter));
if($settings->ldap_tls) {
$this->line("# adding STARTTLS option");
$output[] = "-Z";
}
$output[] = "-v";
$this->line("\n");
$this->line(implode(" \\\n",$output));
exit(0);
}
if(!$this->option('force')) {
$confirmation = $this->confirm('WARNING: This command will make several attempts to connect to your LDAP server. Are you sure this is ok?');
if(!$confirmation) {
$this->error('ABORTING');
exit(-1);
}
}
//$this->line(print_r($settings,true));
$this->info("STAGE 1: Checking settings");
if(!$settings->ldap_enabled) {
$this->error("WARNING: Snipe-IT's LDAP setting is not turned on. (That may be OK if you're still trying to figure out settings)");
}
$ldap_conn = false;
try {
$ldap_conn = ldap_connect($settings->ldap_server);
} catch (Exception $e) {
$this->error("WARNING: Exception caught when executing 'ldap_connect()' - ".$e->getMessage().". We will try to guess.");
}
if(!$ldap_conn) {
$this->error("WARNING: LDAP Server setting of: ".$settings->ldap_server." cannot be parsed. We will try to guess.");
//exit(-1);
}
//since we never use $ldap_conn again, we don't have to ldap_unbind() it (it's not even connected, tbh - that only happens at bind-time)
$parsed = parse_url($settings->ldap_server);
if(@$parsed['scheme'] != 'ldap' && @$parsed['scheme'] != 'ldaps') {
$this->error("WARNING: LDAP URL Scheme of '".@$parsed['scheme']."' is probably incorrect; should usually be ldap or ldaps");
}
if(!@$parsed['host']) {
$this->error("ERROR: Cannot determine hostname or IP from ldap URL: ".$settings->ldap_server.". ABORTING.");
exit(-1);
} else {
$this->info("Determined LDAP hostname to be: ".$parsed['host']);
}
$this->info("Performing DNS lookup of: ".$parsed['host']);
$ips = dns_get_record($parsed['host']);
$raw_ips = [];
//$this->info("Host IP is: ".print_r($ips,true));
if(!$ips || count($ips) == 0) {
$this->error("ERROR: DNS lookup of host: ".$parsed['host']." has failed. ABORTING.");
exit(-1);
}
$this->debugout("IP's? ".print_r($ips,true));
foreach($ips as $ip) {
if(!isset($ip['ip'])) {
continue;
}
$raw_ips[]=$ip['ip'];
if($ip['ip'] == "127.0.0.1") {
$this->error("WARNING: Using the localhost IP as the LDAP server. This is usually wrong");
}
if(ip_in_range($ip['ip'],'10.0.0.0/8') || ip_in_range($ip['ip'],'192.168.0.0/16') || ip_in_range($ip['ip'], '172.16.0.0/12')) {
$this->error("WARNING: Using an RFC1918 Private address for LDAP server. This may be correct, but it can be a problem if your Snipe-IT instance is not hosted on your private network");
}
}
$this->info("STAGE 2: Checking basic network connectivity");
$ports = [389,636];
if(@$parsed['port'] && !in_array($parsed['port'],$ports)) {
$ports[] = $parsed['port'];
}
$open_ports=[];
foreach($ports as $port ) {
$errno = 0;
$errstr = '';
$timeout = 30.0;
$result = '';
$this->info("Attempting to connect to port: ".$port." - may take up to $timeout seconds");
try {
$result = fsockopen($parsed['host'], $port, $errno, $errstr, 30.0);
} catch(Exception $e) {
$this->error("Exception: ".$e->getMessage());
}
if($result) {
$this->info("Success!");
$open_ports[] = $port;
} else {
$this->error("WARNING: Cannot connect to port: $port - $errstr ($errno)");
}
}
if(count($open_ports) == 0) {
$this->error("ERROR - no open ports. ABORTING.");
exit(-1);
}
$this->info("STAGE 3: Determine encryption algorithm, if any");
$ldap_urls = [];
$pretty_ldap_urls = [];
foreach($open_ports as $port) {
$this->line("Trying TLS first for port $port");
$ldap_url = "ldaps://".$parsed['host'].":$port";
if($this->test_anonymous_bind($ldap_url)) {
$this->info("Anonymous bind succesful to $ldap_url!");
$ldap_urls[] = [ $ldap_url, true, false ];
$pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ];
continue; // TODO - lots of copypasta in these if(test_anonymous_bind()) routines...
} else {
$this->error("WARNING: Failed to bind to $ldap_url - trying without certificate checks.");
}
if($this->test_anonymous_bind($ldap_url, false)) {
$this->info("Anonymous bind succesful to $ldap_url with certifcate-checks disabled");
$ldap_urls[] = [ $ldap_url, false, false ];
$pretty_ldap_urls[] = [ $ldap_url, "no", "no" ];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with certificate checks disabled. Trying unencrypted with STARTTLS");
}
$ldap_url = "ldap://".$parsed['host'].":$port";
if($this->test_anonymous_bind($ldap_url, true, true)) {
$this->info("Plain connection to $ldap_url with STARTTLS succesful!");
$ldap_urls[] = [ $ldap_url, true, true ];
$pretty_ldap_urls[] = [ $ldap_url, "YES", "YES" ];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url with STARTTLS enabled. Trying without STARTTLS");
}
if($this->test_anonymous_bind($ldap_url)) {
$this->info("Plain connection to $ldap_url succesful!");
$ldap_urls[] = [ $ldap_url, true, false ];
$pretty_ldap_urls[] = [ $ldap_url, "YES", "no" ];
continue;
} else {
$this->error("WARNING: Failed to bind to $ldap_url. Giving up on port $port");
}
}
$this->debugout(print_r($ldap_urls,true));
if(count($ldap_urls) > 0 ) {
$this->info("Found working LDAP URL's: ");
foreach($ldap_urls as $ldap_url) { // TODO maybe do this as a $this->table() instead?
$this->info("LDAP URL: ".$ldap_url[0]);
$this->info($ldap_url[0]. ($ldap_url[1] ? " certificate checks enabled" : " certificate checks disabled"). ($ldap_url[2] ? " STARTTLS Enabled ": " STARTTLS Disabled"));
}
$this->table(["URL", "Cert Checks Enabled?", "STARTTLS Enabled?"],$pretty_ldap_urls);
} else {
$this->error("ERROR - no valid LDAP URL's available - ABORTING");
exit(1);
}
$this->info("STAGE 4: Test Administrative Bind for LDAP Sync");
foreach($ldap_urls AS $ldap_url) {
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $settings->ldap_uname, Crypt::decrypt($settings->ldap_pword));
}
$this->info("STAGE 5: Test BaseDN");
//grab all LDAP_ constants and fill up a reversed array mapping from weird LDAP dotted-strings to (Constant Name)
$all_defined_constants = get_defined_constants();
$ldap_constants = [];
foreach($all_defined_constants AS $key => $val) {
if(starts_with($key,"LDAP_") && is_string($val)) {
$ldap_constants[$val] = $key; // INVERT the meaning here!
}
}
$this->debugout("LDAP constants are: ".print_r($ldap_constants,true));
foreach($ldap_urls AS $ldap_url) {
if($this->test_informational_bind($ldap_url[0],$ldap_url[1],$ldap_url[2],$settings->ldap_uname,Crypt::decrypt($settings->ldap_pword),$settings)) {
$this->info("Success getting informational bind!");
} else {
$this->error("Unable to get information from bind.");
}
}
$this->info("STAGE 6: Test LDAP Login to Snipe-IT");
foreach($ldap_urls AS $ldap_url) {
$this->info("Starting auth to ".$ldap_url[0]);
while(true) {
$with_tls = $ldap_url[1] ? "with": "without";
$with_startssl = $ldap_url[2] ? "using": "not using";
if(!$this->confirm('Do you wish to try to authenticate to this directory: '.$ldap_url[0]." $with_tls TLS and $with_startssl STARTSSL?")) {
break;
}
$username = $this->ask("Username");
$password = $this->secret("Password");
$this->test_authed_bind($ldap_url[0], $ldap_url[1], $ldap_url[2], $username, $password); // FIXME - should do some other stuff here, maybe with the concatenating or something? maybe? and/or should put up some results?
}
}
$this->info("LDAP TROUBLESHOOTING COMPLETE!");
}
public function connect_to_ldap($ldap_url, $check_cert, $start_tls)
{
$lconn = ldap_connect($ldap_url);
ldap_set_option($lconn, LDAP_OPT_PROTOCOL_VERSION, 3); // should we 'test' different protocol versions here? Does anyone even use anything other than LDAPv3?
// no - it's formally deprecated: https://tools.ietf.org/html/rfc3494
if(!$check_cert) {
putenv('LDAPTLS_REQCERT=never'); // This is horrible; is this *really* the only way to do it?
} else {
putenv('LDAPTLS_REQCERT'); // have to very explicitly and manually *UN* set the env var here to ensure it works
}
if($this->settings->ldap_client_tls_cert && $this->settings->ldap_client_tls_key) {
// client-side TLS certificate support for LDAP (Google Secure LDAP)
putenv('LDAPTLS_CERT=storage/ldap_client_tls.cert');
putenv('LDAPTLS_KEY=storage/ldap_client_tls.key');
}
if($start_tls) {
if(!ldap_start_tls($lconn)) {
$this->error("WARNING: Unable to start TLS");
return false;
}
}
if(!$lconn) {
$this->error("WARNING: Failed to generate connection string - using: ".$ldap_url);
return false;
}
$net = ldap_set_option($lconn, LDAP_OPT_NETWORK_TIMEOUT, $this->option('timeout'));
$time = ldap_set_option($lconn, LDAP_OPT_TIMELIMIT, $this->option('timeout'));
if(!$net || !$time) {
$this->error("Unable to set timeouts!");
}
return $lconn;
}
public function test_anonymous_bind($ldap_url, $check_cert = true, $start_tls = false)
{
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert , $start_tls) {
try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$this->info("gonna try to bind now, this can take a while if we mess it up");
$bind_results = ldap_bind($lconn);
$this->info("Bind results are: ".$bind_results." which translate into boolean: ".(bool)$bind_results);
return (bool)$bind_results;
} catch (Exception $e) {
$this->error("WARNING: Exception caught during bind - ".$e->getMessage());
return false;
}
});
}
public function test_authed_bind($ldap_url, $check_cert, $start_tls, $username, $password)
{
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls, $username, $password) {
try {
$lconn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$bind_results = ldap_bind($lconn, $username, $password);
if(!$bind_results) {
$this->error("WARNING: Failed to bind to $ldap_url as $username");
return false;
} else {
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
return (bool)$lconn;
}
} catch (Exception $e) {
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
return false;
}
});
}
public function test_informational_bind($ldap_url, $check_cert, $start_tls, $username, $password,$settings)
{
return $this->timed_boolean_execute(function () use ($ldap_url, $check_cert, $start_tls, $username, $password, $settings) {
try { // TODO - copypasta'ed from test_authed_bind
$conn = $this->connect_to_ldap($ldap_url, $check_cert, $start_tls);
$bind_results = ldap_bind($conn, $username, $password);
if(!$bind_results) {
$this->error("WARNING: Failed to bind to $ldap_url as $username");
return false;
}
$this->info("SUCCESS - Able to bind to $ldap_url as $username");
$result = ldap_read($conn, '', '(objectClass=*)'/* , ['supportedControl']*/);
$results = ldap_get_entries($conn, $result);
$cleaned_results = $this->ldap_results_cleaner($results);
$this->line(print_r($cleaned_results,true));
//okay, great - now how do we display those results? I have no idea.
// I don't see why this throws an Exception for Google LDAP, but I guess we ought to try and catch it?
$this->comment("I guess we're trying to do the ldap search here, but sometimes it takes too long?");
$this->debugout("Base DN is: ".$settings->ldap_basedn." and filter is: ".parenthesized_filter($settings->ldap_filter));
$search_results = ldap_search($conn, $settings->ldap_basedn, parenthesized_filter($settings->ldap_filter));
$this->info("Printing first 10 results: ");
for($i=0;$i<10;$i++) {
$this->info($search_results[$i]);
}
} catch (\Exception $e) {
$this->error("WARNING: Exception caught during Authed bind to $username - ".$e->getMessage());
return false;
}
});
}
/***********************************************
*
* This function executes $function - which is expected to be some kind of executable function -
* with a timeout set. It respects the timeout by forking execution and setting a strict timer
* for which to get back a SIGUSR1 or SIGUSR2 signal from the forked process.
*
***********************************************/
private function timed_boolean_execute($function)
{
if(!(function_exists('pcntl_sigtimedwait') && function_exists('posix_getpid') && function_exists('pcntl_fork') && function_exists('posix_kill') && function_exists('pcntl_wifsignaled'))) {
// POSIX functions needed for forking aren't present, just run the function inline (ignoring timeout)
$this->info('WARNING: Unable to execute POSIX fork() commands, timeout may not be respected');
return $function();
} else {
$parent_pid = posix_getpid();
$pid = pcntl_fork();
switch($pid) {
case 0:
//we're the 'child'
if($function()) {
//SUCCESS = SIGUSR1
posix_kill($parent_pid, SIGUSR1);
} else {
//FAILURE = SIGUSR2
posix_kill($parent_pid, SIGUSR2);
}
exit();
break; //yes I know we don't need it.
case -1:
//couldn't fork
$this->error("COULD NOT FORK - assuming failure");
return false;
break; //I still know that we don't need it
default:
//we remain the 'parent', $pid is the PID of the forked process.
$siginfo = [];
$exit_status = pcntl_sigtimedwait ([SIGUSR1, SIGUSR2], $siginfo, $this->option('timeout'));
if ($exit_status == SIGUSR1) {
return true;
} else {
posix_kill($pid, SIGKILL); //make sure we don't have processes hanging around that might try and send signals during later executions, confusing us
return false;
}
break; //Yeah I get it already, shush.
}
}
}
}
@@ -1,132 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Events\UserMerged;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
class MergeUsersByUsername extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:merge-users';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command allows you to merge the history of users. It looks for users without an email address as their username and merges them into the version that does have an email username.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Get the list of users who have an email address as their username
$users = User::where('username', 'LIKE', '%@%')->whereNull('deleted_at')->get();
$this->info($users->count().' total non-deleted users whose usernames contain a @ symbol.');
foreach ($users as $user) {
$parts = explode('@', trim($user->username));
$this->info('Checking against username '.trim($parts[0]).'.');
$bad_users = User::where('username', '=', trim($parts[0]))
->whereNull('deleted_at')
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations','uploads', 'acceptances')
->get();
foreach ($bad_users as $bad_user) {
$this->info($bad_user->username.' ('.$bad_user->id.') will be merged into '.$user->username.' ('.$user->id.') ');
// Walk the list of assets
foreach ($bad_user->assets as $asset) {
$this->info('Updating asset '.$asset->asset_tag.' '.$asset->id.' to user '.$user->id);
$asset->assigned_to = $user->id;
if (! $asset->save()) {
$this->error('Could not update assigned_to field on asset '.$asset->asset_tag.' '.$asset->id.' to user '.$user->id);
$this->error('Error saving: '.$asset->getErrors());
}
}
// Walk the list of licenses
foreach ($bad_user->licenses as $license) {
$this->info('Updating license '.$license->name.' '.$license->id.' to user '.$user->id);
$bad_user->licenses()->updateExistingPivot($license->id, ['assigned_to' => $user->id]);
}
// Walk the list of consumables
foreach ($bad_user->consumables as $consumable) {
$this->info('Updating consumable '.$consumable->id.' to user '.$user->id);
$bad_user->consumables()->updateExistingPivot($consumable->id, ['assigned_to' => $user->id]);
}
// Walk the list of accessories
foreach ($bad_user->accessories as $accessory) {
$this->info('Updating accessory '.$accessory->id.' to user '.$user->id);
$bad_user->accessories()->updateExistingPivot($accessory->id, ['assigned_to' => $user->id]);
}
// Walk the list of logs
foreach ($bad_user->userlog as $log) {
$this->info('Updating action log record '.$log->id.' to user '.$user->id);
$log->target_id = $user->id;
$log->save();
}
// Update any manager IDs
$this->info('Updating managed user records to user '.$user->id);
User::where('manager_id', '=', $bad_user->id)->update(['manager_id' => $user->id]);
// Update location manager IDs
foreach ($bad_user->managedLocations as $managedLocation) {
$this->info('Updating managed location record '.$managedLocation->name.' to manager '.$user->id);
$managedLocation->manager_id = $user->id;
$managedLocation->save();
}
foreach ($bad_user->uploads as $upload) {
$this->info('Updating upload log record '.$upload->id.' to user '.$user->id);
$upload->item_id = $user->id;
$upload->save();
}
foreach ($bad_user->acceptances as $acceptance) {
$this->info('Updating acceptance log record '.$acceptance->id.' to user '.$user->id);
$acceptance->item_id = $user->id;
$acceptance->save();
}
// Mark the user as deleted
$this->info('Marking the user as deleted');
$bad_user->deleted_at = Carbon::now()->timestamp;
$bad_user->save();
event(new UserMerged($bad_user, $user, null));
}
}
}
}
@@ -1,168 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
class MoveUploadsToNewDisk extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:move-uploads {delete_local?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This will move your locally uploaded files to whatever your current disk is.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (config('filesystems.default') == 'local') {
$this->error('Your current disk is set to local so we cannot proceed.');
$this->warn("Please configure your .env settings for S3. \nChange your PUBLIC_FILESYSTEM_DISK value to 's3_public' and your PRIVATE_FILESYSTEM_DISK to s3_private.");
return false;
}
$delete_local = $this->argument('delete_local');
$public_uploads['accessories'] = glob('public/uploads/accessories'."/*.*");
$public_uploads['assets'] = glob('public/uploads/assets'."/*.*");
$public_uploads['avatars'] = glob('public/uploads/avatars'."/*.*");
$public_uploads['categories'] = glob('public/uploads/categories'."/*.*");
$public_uploads['companies'] = glob('public/uploads/companies'."/*.*");
$public_uploads['components'] = glob('public/uploads/components'."/*.*");
$public_uploads['consumables'] = glob('public/uploads/consumables'."/*.*");
$public_uploads['departments'] = glob('public/uploads/departments'."/*.*");
$public_uploads['locations'] = glob('public/uploads/locations'."/*.*");
$public_uploads['manufacturers'] = glob('public/uploads/manufacturers'."/*.*");
$public_uploads['suppliers'] = glob('public/uploads/suppliers'."/*.*");
$public_uploads['assetmodels'] = glob('public/uploads/models'."/*.*");
// iterate files
foreach ($public_uploads as $public_type => $public_upload) {
$type_count = 0;
$this->info('- There are ' . count($public_upload) . ' PUBLIC ' . $public_type . ' files.');
for ($i = 0; $i < count($public_upload); $i++) {
$type_count++;
$filename = basename($public_upload[$i]);
try {
Storage::disk('public')->put('uploads/'.$public_type.'/'.$filename, file_get_contents($public_upload[$i]));
$new_url = Storage::disk('public')->url('uploads/'.$public_type.'/'.$filename, $filename);
$this->info($type_count.'. PUBLIC: '.$filename.' was copied to '.$new_url);
} catch (\Exception $e) {
Log::debug($e);
$this->error($e);
}
}
}
$logos = glob("public/uploads/setting*.*");
$this->info("- There are ".count($logos).' files that might be logos.');
$type_count = 0;
foreach ($logos as $logo) {
$this->info($logo);
$type_count++;
$filename = basename($logo);
Storage::disk('public')->put('uploads/' . $filename, file_get_contents($logo));
$this->info($type_count . '. LOGO: ' . $filename . ' was copied to ' . env('PUBLIC_AWS_URL') . '/uploads/' . $filename);
}
$private_uploads['assets'] = glob('storage/private_uploads/assets'."/*.*");
$private_uploads['signatures'] = glob('storage/private_uploads/signatures'."/*.*");
$private_uploads['audits'] = glob('storage/private_uploads/audits'."/*.*");
$private_uploads['assetmodels'] = glob('storage/private_uploads/assetmodels'."/*.*");
$private_uploads['imports'] = glob('storage/private_uploads/imports'."/*.*");
$private_uploads['licenses'] = glob('storage/private_uploads/licenses'."/*.*");
$private_uploads['users'] = glob('storage/private_uploads/users'."/*.*");
$private_uploads['backups'] = glob('storage/private_uploads/backups'."/*.*");
foreach ($private_uploads as $private_type => $private_upload) {
{
$this->info('- There are ' . count($private_upload) . ' PRIVATE ' . $private_type . ' files.');
$type_count = 0;
for ($x = 0; $x < count($private_upload); $x++) {
$type_count++;
$filename = basename($private_upload[$x]);
try {
Storage::put($private_type . '/' . $filename, file_get_contents($private_upload[$i]));
$new_url = Storage::url($private_type . '/' . $filename, $filename);
$this->info($type_count . '. PRIVATE: ' . $filename . ' was copied to ' . $new_url);
} catch (\Exception $e) {
Log::debug($e);
$this->error($e);
}
}
}
if ($delete_local == 'true') {
$public_delete_count = 0;
$private_delete_count = 0;
$this->info("\n\n");
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
$this->warn("\nTHIS WILL DELETE ALL OF YOUR LOCAL UPLOADED FILES. \n\nThis cannot be undone, so you should take a backup of your system before you proceed.\n");
$this->error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
if ($this->confirm('Do you wish to continue?')) {
foreach ($public_uploads as $public_type => $public_upload) {
for ($i = 0; $i < count($public_upload); $i++) {
$filename = $public_upload[$i];
try {
unlink($filename);
$public_delete_count++;
} catch (\Exception $e) {
Log::debug($e);
$this->error($e);
}
}
}
foreach ($private_uploads as $private_type => $private_upload) {
for ($i = 0; $i < count($private_upload); $i++) {
$filename = $private_upload[$i];
try {
unlink($filename);
$private_delete_count++;
} catch (\Exception $e) {
Log::debug($e);
$this->error($e);
}
}
}
$this->info($public_delete_count . ' PUBLIC local files and ' . $private_delete_count . ' PRIVATE local files were deleted from your filesystem.');
}
}
}
}
}
@@ -1,52 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
class NormalizeUserNames extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:normalize-names';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Normalizes weirdly formatted names as first-letter upercased';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$users = User::get();
$this->info($users->count() . ' users');
foreach ($users as $user) {
$user->first_name = ucwords(strtolower($user->first_name));
$user->last_name = ucwords(strtolower($user->last_name));
$user->email = strtolower($user->email);
$user->save();
}
}
}
@@ -1,160 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Illuminate\Support\Facades\Log;
ini_set('max_execution_time', env('IMPORT_TIME_LIMIT', 600)); //600 seconds = 10 minutes
ini_set('memory_limit', env('IMPORT_MEMORY_LIMIT', '500M'));
/**
* Class ObjectImportCommand
*/
class ObjectImportCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'snipeit:import';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import Items from CSV';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
private $bar;
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$filename = $this->argument('filename');
$class = title_case($this->option('item-type'));
$classString = "App\\Importer\\{$class}Importer";
$importer = new $classString($filename);
$importer->setCallbacks([$this, 'log'], [$this, 'progress'], [$this, 'errorCallback'])
->setUserId($this->option('user_id'))
->setUpdating($this->option('update'))
->setShouldNotify($this->option('send-welcome'))
->setUsernameFormat($this->option('username_format'));
// This $logFile/useFiles() bit is currently broken, so commenting it out for now
// $logFile = $this->option('logfile');
// Log::useFiles($logFile);
$this->comment('======= Importing Items from '.$filename.' =========');
$importer->import();
$this->bar = null;
if (! empty($this->errors)) {
$this->comment('The following Errors were encountered.');
foreach ($this->errors as $asset => $error) {
$this->comment('Error: Item: '.$asset.' failed validation: '.json_encode($error));
}
} else {
$this->comment('All Items imported successfully!');
}
$this->comment('');
}
public function errorCallback($item, $field, $errorString)
{
$this->errors[$item->name][$field] = $errorString;
}
public function progress($count)
{
if (! $this->bar) {
$this->bar = $this->output->createProgressBar($count);
}
static $index = 0;
$index++;
if ($index < $count) {
$this->bar->advance();
} else {
$this->bar->finish();
}
}
// Tracks the current item for error messages
private $updating;
// An array of errors encountered while parsing
private $errors;
/**
* Log a message to file, configurable by the --log-file parameter.
* If a warning message is passed, we'll spit it to the console as well.
*
* @author Daniel Melzter
* @since 3.0
* @param string $string
* @param string $level
*/
public function log($string, $level = 'info')
{
if ($level === 'warning') {
Log::warning($string);
$this->comment($string);
} else {
Log::Info($string);
if ($this->option('verbose')) {
$this->comment($string);
}
}
}
/**
* Get the console command arguments.
*
* @author Daniel Melzter
* @since 3.0
* @return array
*/
protected function getArguments()
{
return [
['filename', InputArgument::REQUIRED, 'File for the CSV import.'],
];
}
/**
* Get the console command options.
*
* @author Daniel Melzter
* @since 3.0
* @return array
*/
protected function getOptions()
{
return [
['email_format', null, InputOption::VALUE_REQUIRED, 'The format of the email addresses that should be generated. Options are firstname.lastname, firstname, filastname', null],
['username_format', null, InputOption::VALUE_REQUIRED, 'The format of the username that should be generated. Options are firstname.lastname, firstname, filastname, email', null],
['logfile', null, InputOption::VALUE_REQUIRED, 'The path to log output to. storage/logs/importer.log by default', storage_path('logs/importer.log')],
['item-type', null, InputOption::VALUE_REQUIRED, 'Item Type To import. Valid Options are Asset, Consumable, Accessory, License, or User', 'Asset'],
['web-importer', null, InputOption::VALUE_NONE, 'Internal: packages output for use with the web importer'],
['user_id', null, InputOption::VALUE_REQUIRED, 'ID of user creating items', 1],
['update', null, InputOption::VALUE_NONE, 'If a matching item is found, update item information'],
['send-welcome', null, InputOption::VALUE_NONE, 'Whether to send a welcome email to any new users that are created.'],
];
}
}
-91
View File
@@ -1,91 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use Schema;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
class PaveIt extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:pave {--force : Skip the interactive yes/no prompt for confirmation}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear the database tables, leaving all migrations, table structure, and the first user in place. (It is primarily a quick tool for developers.) If you want to destroy all tables as well, use php artisan db:wipe.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!$this->option('force')) {
$confirmation = $this->confirm("\n****************************************************\nTHIS WILL DELETE ALL OF THE DATA IN YOUR DATABASE. \nThere is NO undo. This WILL destroy ALL of your data, \nINCLUDING ANY non-Snipe-IT tables you have in this database. \n****************************************************\n\nDo you wish to continue? No backsies! ");
if (!$confirmation) {
$this->error('ABORTING');
exit(-1);
}
}
// List all the tables in the database so we don't have to worry about missing some as the app grows
$tables = DB::connection()->getDoctrineSchemaManager()->listTableNames();
$except_tables = [
'oauth_access_tokens',
'oauth_clients',
'oauth_personal_access_clients',
'migrations',
'settings',
'users',
];
// We only need to find out what these are so we can nuke these columns on the assets table.
$custom_fields = CustomField::get();
foreach ($custom_fields as $custom_field) {
$this->info('DROP the '.$custom_field->db_column.' column from assets as well.');
if (\Schema::hasColumn('assets', $custom_field->db_column)) {
\Schema::table('assets', function ($table) use ($custom_field) {
$table->dropColumn($custom_field->db_column);
});
}
}
foreach ($tables as $table) {
if (in_array($table, $except_tables)) {
$this->info($table. ' is SKIPPED.');
} else {
\DB::statement('truncate '.$table);
$this->info($table. ' is TRUNCATED.');
}
}
// Leave in the demo oauth keys so we don't have to reset them every day in the demos
\DB::statement('delete from oauth_clients WHERE id > 2');
\DB::statement('delete from oauth_access_tokens WHERE id > 2');
}
}
-168
View File
@@ -1,168 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Category;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Location;
use App\Models\Manufacturer;
use App\Models\Statuslabel;
use App\Models\Supplier;
use App\Models\User;
use Illuminate\Console\Command;
class Purge extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:purge {--force=false}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Purge all soft-deleted deleted records in the database. This will rewrite history for items that have been edited, or checked in or out. It will also rewrite history for users associated with deleted items.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$force = $this->option('force');
if (($this->confirm("\n****************************************************\nTHIS WILL PURGE ALL SOFT-DELETED ITEMS IN YOUR SYSTEM. \nThere is NO undo. This WILL permanently destroy \nALL of your deleted data. \n****************************************************\n\nDo you wish to continue? No backsies! [y|N]")) || $force == 'true') {
/**
* Delete assets
*/
$assets = Asset::whereNotNull('deleted_at')->withTrashed()->get();
$assetcount = $assets->count();
$this->info($assets->count().' assets purged.');
$asset_assoc = 0;
$asset_maintenances = 0;
foreach ($assets as $asset) {
$this->info('- Asset "'.$asset->present()->name().'" deleted.');
$asset_assoc += $asset->assetlog()->count();
$asset->assetlog()->forceDelete();
$asset_maintenances += $asset->assetmaintenances()->count();
$asset->assetmaintenances()->forceDelete();
$asset->forceDelete();
}
$this->info($asset_assoc.' corresponding log records purged.');
$this->info($asset_maintenances.' corresponding maintenance records purged.');
$locations = Location::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($locations->count().' locations purged.');
foreach ($locations as $location) {
$this->info('- Location "'.$location->name.'" deleted.');
$location->forceDelete();
}
$accessories = Accessory::whereNotNull('deleted_at')->withTrashed()->get();
$accessory_assoc = 0;
$this->info($accessories->count().' accessories purged.');
foreach ($accessories as $accessory) {
$this->info('- Accessory "'.$accessory->name.'" deleted.');
$accessory_assoc += $accessory->assetlog()->count();
$accessory->assetlog()->forceDelete();
$accessory->forceDelete();
}
$this->info($accessory_assoc.' corresponding log records purged.');
$consumables = Consumable::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($consumables->count().' consumables purged.');
foreach ($consumables as $consumable) {
$this->info('- Consumable "'.$consumable->name.'" deleted.');
$consumable->assetlog()->forceDelete();
$consumable->forceDelete();
}
$components = Component::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($components->count().' components purged.');
foreach ($components as $component) {
$this->info('- Component "'.$component->name.'" deleted.');
$component->assetlog()->forceDelete();
$component->forceDelete();
}
$licenses = License::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($licenses->count().' licenses purged.');
foreach ($licenses as $license) {
$this->info('- License "'.$license->name.'" deleted.');
$license->assetlog()->forceDelete();
$license->licenseseats()->forceDelete();
$license->forceDelete();
}
$models = AssetModel::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($models->count().' asset models purged.');
foreach ($models as $model) {
$this->info('- Asset Model "'.$model->name.'" deleted.');
$model->forceDelete();
}
$categories = Category::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($categories->count().' categories purged.');
foreach ($categories as $category) {
$this->info('- Category "'.$category->name.'" deleted.');
$category->forceDelete();
}
$suppliers = Supplier::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($suppliers->count().' suppliers purged.');
foreach ($suppliers as $supplier) {
$this->info('- Supplier "'.$supplier->name.'" deleted.');
$supplier->forceDelete();
}
$users = User::whereNotNull('deleted_at')->where('show_in_list', '!=', '0')->withTrashed()->get();
$this->info($users->count().' users purged.');
$user_assoc = 0;
foreach ($users as $user) {
$this->info('- User "'.$user->username.'" deleted.');
$user_assoc += $user->userlog()->count();
$user->userlog()->forceDelete();
$user->forceDelete();
}
$this->info($user_assoc.' corresponding user log records purged.');
$manufacturers = Manufacturer::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($manufacturers->count().' manufacturers purged.');
foreach ($manufacturers as $manufacturer) {
$this->info('- Manufacturer "'.$manufacturer->name.'" deleted.');
$manufacturer->forceDelete();
}
$status_labels = Statuslabel::whereNotNull('deleted_at')->withTrashed()->get();
$this->info($status_labels->count().' status labels purged.');
foreach ($status_labels as $status_label) {
$this->info('- Status Label "'.$status_label->name.'" deleted.');
$status_label->forceDelete();
}
} else {
$this->info('Action canceled. Nothing was purged.');
}
}
}
@@ -1,44 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class PurgeLoginAttempts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:purge-logins';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clears the login_attempts table';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->confirm("\n****************************************************\nTHIS WILL DELETE ALL OF THE YOUR LOGIN ATTEMPT RECORDS. \nThere is NO undo! \n****************************************************\n\nDo you wish to continue? No backsies! [y|N]")) {
\DB::statement('delete from login_attempts');
}
}
}
@@ -1,131 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\CustomField;
use Illuminate\Console\Command;
class ReEncodeCustomFieldNames extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:regenerate-fieldnames';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This utility will regenerate the column names for custom fields. It should typically only be needed when a PHP upgrade changed the behavior of the unicode conversion between versions.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* All three of these things must match for the custom fields system to work as expected:
*
* - what the system thinks the output of $field->convertUnicodeDbSlug() is
* - the actual db_column name in the customfields table
* - the physical column name that was created on the assets table
*
* For some people who upgraded their version of PHP, the unicode converter now behaves
* differently in than it did when their custom fields were first created, specifically as it
* relates to handling slashes, ampersands, etc. This can result in the field names no longer
* matching up, as an older version of the PHP extension simply dropped slashes, etc, while the
* newer version of the PHP extension will convert them to underscores.
*
* @return mixed
*/
public function handle()
{
if ($this->confirm('This will regenerate all of the custom field database fieldnames in your database. THIS WILL CHANGE YOUR SCHEMA AND SHOULD NOT BE DONE WITHOUT MAKING A BACKUP FIRST. Do you wish to continue?')) {
/** Get all of the custom fields */
$fields = CustomField::get();
$asset_columns = \DB::getSchemaBuilder()->getColumnListing('assets');
$custom_field_columns = [];
/** Loop through the columns on the assets table */
foreach ($asset_columns as $asset_column) {
/** Add ones that start with _snipeit_ to an array for handling */
if (strpos($asset_column, '_snipeit_') === 0) {
/**
* Get the ID of the custom field based on the fieldname.
* For example, in _snipeit_mac_address_1, we grab the 1 because we know
* that's the ID of the custom field that created the column.
* Then use that ID as the array key for use comparing the actual assets field name
* and the db_column value from the custom fields table.
*/
$last_part = substr(strrchr($asset_column, '_snipeit_'), 1);
$custom_field_columns[$last_part] = $asset_column;
}
}
foreach ($fields as $field) {
$this->info($field->name.' ('.$field->id.') column should be '.$field->convertUnicodeDbSlug());
/** The assets table has the column it should have, all is well */
if ($field->db_column == $field->convertUnicodeDbSlug() && \Schema::hasColumn('assets', $field->convertUnicodeDbSlug())) {
$this->info('-- ✓ This field exists on the assets table and the value for db_column matches in the custom_fields table.');
/**
* There is a mismatch between the fieldname on the assets table and
* what $field->convertUnicodeDbSlug() is *now* expecting.
*/
} else {
if ($field->db_column != $field->convertUnicodeDbSlug()) {
$this->error('-- ✘ Field mismatch: '.$field->name.' value should be '.$field->convertUnicodeDbSlug().' but is '.$field->db_column.' in the custom_fields table');
} else {
$this->error('-- ✘ Field mismatch: '.$field->name.' column should be '.$field->convertUnicodeDbSlug().' but is '.$custom_field_columns[$field->id].' on the assets table.');
}
/** Make sure the custom_field_columns array has the ID */
if (array_key_exists($field->id, $custom_field_columns)) {
/**
* Update the asset schema to the corrected fieldname that will be recognized by the
* system elsewhere that we use $field->convertUnicodeDbSlug()
*/
$this->info('-- ✓ Updating field from '.$field->db_column.' to '.$field->convertUnicodeDbSlug().' in the assets table');
\Schema::table('assets', function ($table) use ($custom_field_columns, $field) {
$table->renameColumn($custom_field_columns[$field->id], $field->convertUnicodeDbSlug());
});
$this->info('-- ✓ Updating field from '.$field->db_column.' to '.$field->convertUnicodeDbSlug().' in the custom fields table');
$field->db_column = $field->convertUnicodeDbSlug();
$field->save();
} else {
$this->warn('-- ✘ WARNING: There is no field on the assets table ending in '.$field->id.'. This may require more in-depth investigation and may mean the schema was altered manually.');
}
}
/** Update the db_column property in the custom fields table, just in case it doesn't match the other
* things.
*/
$field->db_column = $field->convertUnicodeDbSlug();
$field->save();
}
}
}
}
-157
View File
@@ -1,157 +0,0 @@
<?php
namespace App\Console\Commands;
use App\LegacyEncrypter\McryptEncrypter;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Setting;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class RecryptFromMcrypt extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:legacy-recrypt
{--force : Force a re-crypt of encrypted data from MCRYPT.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command allows upgrading users to de-encrypt their deprecated mcrypt encrypted fields and re-encrypt them using the current OpenSSL encryption.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Check and see if they have a legacy app key listed in their .env
// If not, we can try to use the current APP_KEY if looks like it's old
$legacy_key = env('LEGACY_APP_KEY');
$key_parts = explode(':', $legacy_key);
$legacy_cipher = env('LEGACY_CIPHER', 'rijndael-256');
$errors = [];
if (! $legacy_key) {
$this->error('ERROR: You do not have a LEGACY_APP_KEY set in your .env file. Please locate your old APP_KEY and ADD a line to your .env file like: LEGACY_APP_KEY=YOUR_OLD_APP_KEY');
return false;
}
// Do some basic legacy app key length checks
if (strlen($legacy_key) == 32) {
$legacy_length_check = true;
} elseif (array_key_exists('1', $key_parts) && (strlen($key_parts[1]) == 44)) {
$legacy_key = base64_decode($key_parts[1], true);
$legacy_length_check = true;
} else {
$legacy_length_check = false;
}
// Check that the app key is 32 characters
if ($legacy_length_check === true) {
$this->comment('INFO: Your LEGACY_APP_KEY looks correct. Okay to continue.');
} else {
$this->error('ERROR: Your LEGACY_APP_KEY is not the correct length (32 characters or base64 followed by 44 characters for later versions). Please locate your old APP_KEY and use that as your LEGACY_APP_KEY in your .env file to continue.');
return false;
}
$this->error('================================!!!! WARNING !!!!================================');
$this->error('================================!!!! WARNING !!!!================================');
$this->comment("This tool will attempt to decrypt your old Snipe-IT (mcrypt, now deprecated) encrypted data and re-encrypt it using OpenSSL. \n\nYou should only continue if you have backed up any and all old APP_KEYs and have backed up your data.");
$force = ($this->option('force')) ? true : false;
if ($force || ($this->confirm('Are you SURE you wish to continue?'))) {
$backup_file = 'backups/env-backups/'.'app_key-'.date('Y-m-d-gis');
try {
Storage::disk('local')->put($backup_file, 'APP_KEY: '.config('app.key'));
Storage::disk('local')->append($backup_file, 'LEGACY_APP_KEY: '.$legacy_key);
} catch (\Exception $e) {
$this->info('WARNING: Could not backup app keys');
}
if ($legacy_cipher) {
$mcrypter = new McryptEncrypter($legacy_key, $legacy_cipher);
} else {
$mcrypter = new McryptEncrypter($legacy_key);
}
$settings = Setting::getSettings();
if ($settings->ldap_pword == '') {
$this->comment('INFO: No LDAP password found. Skipping... ');
} else {
$decrypted_ldap_pword = $mcrypter->decrypt($settings->ldap_pword);
$settings->ldap_pword = Crypt::encrypt($decrypted_ldap_pword);
$settings->save();
}
/** @var CustomField[] $custom_fields */
$custom_fields = CustomField::where('field_encrypted', '=', 1)->get();
$this->comment('INFO: Retrieving encrypted custom fields...');
$query = Asset::withTrashed();
foreach ($custom_fields as $custom_field) {
$this->comment('FIELD TO RECRYPT: '.$custom_field->name.' ('.$custom_field->db_column.')');
$query->orWhereNotNull($custom_field->db_column);
}
// Get all assets with a value in any of the fields that were encrypted
/** @var Asset[] $assets */
$assets = $query->get();
$bar = $this->output->createProgressBar(count($assets));
foreach ($assets as $asset) {
foreach ($custom_fields as $encrypted_field) {
$columnName = $encrypted_field->db_column;
// Make sure the value isn't null
if ($asset->{$columnName} != '') {
// Try to decrypt the payload using the legacy app key
try {
$decrypted_field = $mcrypter->decrypt($asset->{$columnName});
$asset->{$columnName} = Crypt::encrypt($decrypted_field);
$this->comment($decrypted_field);
} catch (\Exception $e) {
$errors[] = ' - ERROR: Could not decrypt field ['.$encrypted_field->name.']: '.$e->getMessage();
}
}
}
$asset->save();
$bar->advance();
}
$bar->finish();
if (count($errors) > 0) {
$this->comment("\n\n");
$this->error("The decrypter encountered some errors: \n");
foreach ($errors as $error) {
$this->error($error);
}
}
}
}
}
@@ -1,104 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\Setting;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Console\Command;
class RegenerateAssetTags extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:regenerate-tags {--start=} {--output= : info|warn|error|all} ';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This utility will regenerate all asset tags. THIS IS DATA-DESTRUCTIVE AND SHOULD BE USED WITH CAUTION. ';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->confirm('This will regenerate all of the asset tags within your system. This action is data-destructive and should be used with caution. Do you wish to continue?')) {
$output['info'] = [];
$output['warn'] = [];
$output['error'] = [];
$settings = Setting::getSettings();
$start_tag = ($this->option('start')) ? $this->option('start') : (($settings->next_auto_tag_base) ? Setting::getSettings()->next_auto_tag_base : 1);
$this->info('Starting at '.$start_tag);
$total_assets = Asset::orderBy('id', 'asc')->get();
$bar = $this->output->createProgressBar(count($total_assets));
try {
Artisan::call('backup:run');
} catch (\Exception $e) {
$output['error'][] = $e;
}
foreach ($total_assets as $asset) {
$output['info'][] = 'Asset tag:'.$asset->asset_tag;
$asset->asset_tag = $settings->auto_increment_prefix.$settings->auto_increment_prefix.$start_tag;
if ($settings->zerofill_count > 0) {
$asset->asset_tag = $settings->auto_increment_prefix.Asset::zerofill($start_tag, $settings->zerofill_count);
}
$output['info'][] = 'New Asset tag:'.$asset->asset_tag;
// Use forceSave here to override model level validation
$asset->forceSave();
$start_tag++;
if ($bar) {
$bar->advance();
}
}
$settings->next_auto_tag_base = Asset::zerofill($start_tag, $settings->zerofill_count);
$settings->save();
$bar->finish();
$this->info("\n");
if (($this->option('output') == 'all') || ($this->option('output') == 'info')) {
foreach ($output['info'] as $key => $output_text) {
$this->info($output_text);
}
}
if (($this->option('output') == 'all') || ($this->option('output') == 'warn')) {
foreach ($output['warn'] as $key => $output_text) {
$this->warn($output_text);
}
}
if (($this->option('output') == 'all') || ($this->option('output') == 'error')) {
foreach ($output['error'] as $key => $output_text) {
$this->error($output_text);
}
}
}
}
}
@@ -1,90 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Console\Command;
class ResetDemoSettings extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:demo-settings';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This will reset the Snipe-IT demo settings back to default. ';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info('Resetting the demo settings.');
$settings = Setting::first();
$settings->per_page = 20;
$settings->site_name = 'Snipe-IT Asset Management Demo';
$settings->auto_increment_assets = 1;
$settings->logo = 'snipe-logo.png';
$settings->alert_email = 'service@snipe-it.io';
$settings->login_note = 'Use `admin` / `password` to login to the demo.';
$settings->header_color = null;
$settings->barcode_type = 'QRCODE';
$settings->default_currency = 'USD';
$settings->brand = 2;
$settings->ldap_enabled = 0;
$settings->full_multiple_companies_support = 0;
$settings->alt_barcode = 'C128';
$settings->skin = '';
$settings->email_domain = 'snipeitapp.com';
$settings->email_format = 'filastname';
$settings->username_format = 'filastname';
$settings->date_display_format = 'D M d, Y';
$settings->time_display_format = 'g:iA';
$settings->thumbnail_max_h = '30';
$settings->locale = 'en-US';
$settings->version_footer = 'on';
$settings->support_footer = null;
$settings->saml_enabled = '0';
$settings->saml_sp_x509cert = null;
$settings->saml_idp_metadata = null;
$settings->saml_attr_mapping_username = null;
$settings->saml_forcelogin = '0';
$settings->saml_slo = null;
$settings->saml_custom_settings = null;
$settings->save();
if ($user = User::where('username', '=', 'admin')->first()) {
$user->locale = 'en-US';
$user->save();
}
\Storage::disk('public')->put('snipe-logo.png', file_get_contents(public_path('img/demo/snipe-logo.png')));
\Storage::disk('public')->put('snipe-logo-lg.png', file_get_contents(public_path('img/demo/snipe-logo-lg.png')));
}
}
@@ -1,105 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\License;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
class RestoreDeletedUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:restore-users {--start_date=} {--end_date=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Restore users, and any associated assets and license checkouts.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$start_date = $this->option('start_date');
$end_date = $this->option('end_date');
$asset_totals = 0;
$license_totals = 0;
$user_count = 0;
if (($start_date == '') || ($end_date == '')) {
$this->info('ERROR: All fields are required.');
return false;
}
$users = User::whereBetween('deleted_at', [$start_date, $end_date])->withTrashed()->get();
$this->info('There are '.$users->count().' users deleted between '.$start_date.' and '.$end_date);
$this->warn('Making a backup!');
Artisan::call('backup:run');
foreach ($users as $user) {
$user_count++;
$user_logs = Actionlog::where('target_id', $user->id)->where('target_type', User::class)
->where('action_type', 'checkout')->with('item')->get();
$this->info($user_count.'. '.$user->username.' ('.$user->id.') was deleted at '.$user->deleted_at.' and has '.$user_logs->count().' checkouts associated.');
foreach ($user_logs as $user_log) {
$this->info(' * '.$user_log->item_type.': '.$user_log->item->name.' - item_id: '.$user_log->item_id);
if ($user_log->item_type == Asset::class) {
$asset_totals++;
DB::table('assets')
->where('id', $user_log->item_id)
->update(['assigned_to' => $user->id, 'assigned_type'=> User::class]);
$this->info(' ** Asset '.$user_log->item->id.' ('.$user_log->item->asset_tag.') restored to user '.$user->id.'');
} elseif ($user_log->item_type == License::class) {
$license_totals++;
$avail_seat = DB::table('license_seats')->where('license_id', '=', $user_log->item->id)
->whereNull('assigned_to')->whereNull('asset_id')->whereBetween('updated_at', [$start_date, $end_date])->first();
if ($avail_seat) {
$this->info(' ** Allocating seat '.$avail_seat->id.' for this License');
DB::table('license_seats')
->where('id', $avail_seat->id)
->update(['assigned_to' => $user->id]);
} else {
$this->warn('ERROR: No available seats for '.$user_log->item->name);
}
}
}
$this->warn('Restoring user '.$user->username.'!');
$user->restore();
}
$this->info($asset_totals.' assets affected');
$this->info($license_totals.' licenses affected');
}
}
-490
View File
@@ -1,490 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use ZipArchive;
use Illuminate\Support\Facades\Log;
class SQLStreamer {
private $input;
private $output;
// embed the prefix here?
public ?string $prefix;
private bool $reading_beginning_of_line = true;
public static $buffer_size = 1024 * 1024; // use a 1MB buffer, ought to work fine for most cases?
public array $tablenames = [];
private bool $should_guess = false;
private bool $statement_is_permitted = false;
public function __construct($input, $output, string $prefix = null)
{
$this->input = $input;
$this->output = $output;
$this->prefix = $prefix;
}
public function parse_sql(string $line): string {
// take into account the 'start of line or not' setting as an instance variable?
// 'continuation' lines for a permitted statement are PERMITTED.
if($this->statement_is_permitted && $line[0] === ' ') {
return $line;
}
$table_regex = '`?([a-zA-Z0-9_]+)`?';
$allowed_statements = [
"/^(DROP TABLE (?:IF EXISTS )?)`$table_regex(.*)$/" => false,
"/^(CREATE TABLE )$table_regex(.*)$/" => true, //sets up 'continuation'
"/^(LOCK TABLES )$table_regex(.*)$/" => false,
"/^(INSERT INTO )$table_regex(.*)$/" => false,
"/^UNLOCK TABLES/" => false,
// "/^\\) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;/" => false, // FIXME not sure what to do here?
"/^\\)[a-zA-Z0-9_= ]*;$/" => false
// ^^^^^^ that bit should *exit* the 'perimitted' black
];
foreach($allowed_statements as $statement => $statechange) {
// $this->info("Checking regex: $statement...\n");
$matches = [];
if (preg_match($statement,$line,$matches)) {
$this->statement_is_permitted = $statechange;
// matches are: 1 => first part of the statement, 2 => tablename, 3 => rest of statement
// (with of course 0 being "the whole match")
if (@$matches[2]) {
// print "Found a tablename! It's: ".$matches[2]."\n";
if ($this->should_guess) {
@$this->tablenames[$matches[2]] += 1;
continue; //oh? FIXME
} else {
$cleaned_tablename = \DB::getTablePrefix().preg_replace('/^'.$this->prefix.'/','',$matches[2]);
$line = preg_replace($statement,'$1`'.$cleaned_tablename.'`$3' , $line);
}
} else {
// no explicit tablename in this one, leave the line alone
}
//how do we *replace* the tablename?
// print "RETURNING LINE: $line";
return $line;
}
}
// all that is not allowed is denied.
return "";
}
//this is used in exactly *TWO* places, and in both cases should return a prefix I think?
// first - if you do the --sanitize-only one (which is mostly for testing/development)
// next - when you run *without* a guessed prefix, this is run first to figure out the prefix
// I think we have to *duplicate* the call to be able to run it again?
public static function guess_prefix($input):string
{
$parser = new self($input, null);
$parser->should_guess = true;
$parser->line_aware_piping(); // <----- THIS is doing the heavy lifting!
$check_tables = ['settings' => null, 'migrations' => null /* 'assets' => null */]; //TODO - move to statics?
//can't use 'users' because the 'accessories_users' table?
// can't use 'assets' because 'ver1_components_assets'
foreach($check_tables as $check_table => $_ignore) {
foreach ($parser->tablenames as $tablename => $_count) {
// print "Comparing $tablename to $check_table\n";
if (str_ends_with($tablename,$check_table)) {
// print "Found one!\n";
$check_tables[$check_table] = substr($tablename,0,-strlen($check_table));
}
}
}
$guessed_prefix = null;
foreach ($check_tables as $clean_table => $prefix_guess) {
if(is_null($prefix_guess)) {
print("Couldn't find table $clean_table\n");
die();
}
if(is_null($guessed_prefix)) {
$guessed_prefix = $prefix_guess;
} else {
if ($guessed_prefix != $prefix_guess) {
print("Prefix mismatch! Had guessed $guessed_prefix but got $prefix_guess\n");
die();
}
}
}
return $guessed_prefix;
}
public function line_aware_piping(): int
{
$bytes_read = 0;
if (! $this->input) {
throw new \Exception("No Input available for line_aware_piping");
}
while (($buffer = fgets($this->input, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
if ($this->reading_beginning_of_line) {
// Log::debug("Buffer is: '$buffer'");
$cleaned_buffer = $this->parse_sql($buffer);
if ($this->output) {
$bytes_written = fwrite($this->output, $cleaned_buffer);
if ($bytes_written === false) {
throw new \Exception("Unable to write to pipe");
}
}
}
// if we got a newline at the end of this, then the _next_ read is the beginning of a line
if($buffer[strlen($buffer)-1] === "\n") {
$this->reading_beginning_of_line = true;
} else {
$this->reading_beginning_of_line = false;
}
}
return $bytes_read;
}
}
class RestoreFromBackup extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
// FIXME - , stripping prefixes and nonstandard SQL statements. Without --prefix, guess and return the correct prefix to strip
protected $signature = 'snipeit:restore
{--force : Skip the danger prompt; assuming you enter "y"}
{filename : The zip file to be migrated}
{--no-progress : Don\'t show a progress bar}
{--sanitize-guess-prefix : Guess and output the table-prefix needed to "sanitize" the SQL}
{--sanitize-with-prefix= : "Sanitize" the SQL, using the passed-in table prefix (can be learned from --sanitize-guess-prefix). Pass as just \'--sanitize-with-prefix=\' to use no prefix}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Restore from a previously created Snipe-IT backup file';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$dir = getcwd();
if( $dir != base_path() ) { // usually only the case when running via webserver, not via command-line
Log::debug("Current working directory is: $dir, changing directory to: ".base_path());
chdir(base_path()); // TODO - is this *safe* to change on a running script?!
}
//
$filename = $this->argument('filename');
if (! $filename) {
return $this->error('Missing required filename');
}
if (! $this->option('force') && ! $this->option('sanitize-guess-prefix') && ! $this->confirm('Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!')) {
return $this->error('Data loss not confirmed');
}
if (config('database.default') != 'mysql') {
return $this->error('DB_CONNECTION must be MySQL in order to perform a restore. Detected: '.config('database.default'));
}
$za = new ZipArchive();
$errcode = $za->open($filename/* , ZipArchive::RDONLY */); // that constant only exists in PHP 7.4 and higher
if ($errcode !== true) {
$errors = [
ZipArchive::ER_EXISTS => 'File already exists.',
ZipArchive::ER_INCONS => 'Zip archive inconsistent.',
ZipArchive::ER_INVAL => 'Invalid argument.',
ZipArchive::ER_MEMORY => 'Malloc failure.',
ZipArchive::ER_NOENT => 'No such file ('.$filename.') in directory '.$dir.'.',
ZipArchive::ER_NOZIP => 'Not a zip archive.',
ZipArchive::ER_OPEN => "Can't open file.",
ZipArchive::ER_READ => 'Read error.',
ZipArchive::ER_SEEK => 'Seek error.',
];
return $this->error('Could not access file: '.$filename.' - '.array_key_exists($errcode, $errors) ? $errors[$errcode] : " Unknown reason: $errcode");
}
$private_dirs = [
'storage/private_uploads/accessories',
'storage/private_uploads/assetmodels',
'storage/private_uploads/assets', // these are asset _files_, not the pictures.
'storage/private_uploads/audits',
'storage/private_uploads/components',
'storage/private_uploads/consumables',
'storage/private_uploads/eula-pdfs',
'storage/private_uploads/imports',
'storage/private_uploads/licenses',
'storage/private_uploads/signatures',
'storage/private_uploads/users',
];
$private_files = [
'storage/oauth-private.key',
'storage/oauth-public.key',
];
$public_dirs = [
'public/uploads/accessories',
'public/uploads/assets', // these are asset _pictures_, not asset files
'public/uploads/avatars',
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
'public/uploads/categories',
'public/uploads/companies',
'public/uploads/components',
'public/uploads/consumables',
'public/uploads/departments',
'public/uploads/locations',
'public/uploads/manufacturers',
'public/uploads/models',
'public/uploads/suppliers',
];
$public_files = [
'public/uploads/logo.*',
'public/uploads/setting-email_logo*',
'public/uploads/setting-label_logo*',
'public/uploads/setting-logo*',
'public/uploads/favicon.*',
'public/uploads/favicon-uploaded.*',
];
$all_files = $private_dirs + $public_dirs;
$sqlfiles = [];
$sqlfile_indices = [];
$interesting_files = [];
$boring_files = [];
for ($i = 0; $i < $za->numFiles; $i++) {
$stat_results = $za->statIndex($i);
// echo "index: $i\n";
// print_r($stat_results);
$raw_path = $stat_results['name'];
if (strpos($raw_path, '\\') !== false) { //found a backslash, swap it to forward-slash
$raw_path = strtr($raw_path, '\\', '/');
//print "Translating file: ".$stat_results['name']." to: ".$raw_path."\n";
}
// skip macOS resource fork files (?!?!?!)
if (strpos($raw_path, '__MACOSX') !== false && strpos($raw_path, '._') !== false) {
//print "SKIPPING macOS Resource fork file: $raw_path\n";
$boring_files[] = $raw_path;
continue;
}
if (@pathinfo($raw_path, PATHINFO_EXTENSION) == 'sql') {
Log::debug("Found a sql file!");
$sqlfiles[] = $raw_path;
$sqlfile_indices[] = $i;
continue;
}
foreach (array_merge($private_dirs, $public_dirs) as $dir) {
$last_pos = strrpos($raw_path, $dir . '/');
if ($last_pos !== false) {
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
$interesting_files[$raw_path] = ['dest' => $dir, 'index' => $i];
continue 2;
if ($last_pos + strlen($dir) + 1 == strlen($raw_path)) {
// we don't care about that; we just want files with the appropriate prefix
//print("FOUND THE EXACT DIRECTORY: $dir AT: $raw_path!!!\n");
}
}
}
$good_extensions = ['png', 'gif', 'jpg', 'svg', 'jpeg', 'doc', 'docx', 'pdf', 'txt',
'zip', 'rar', 'xls', 'xlsx', 'lic', 'xml', 'rtf', 'webp', 'key', 'ico',];
foreach (array_merge($private_files, $public_files) as $file) {
$has_wildcard = (strpos($file, '*') !== false);
if ($has_wildcard) {
$file = substr($file, 0, -1); //trim last character (which should be the wildcard)
}
$last_pos = strrpos($raw_path, $file); // no trailing slash!
if ($last_pos !== false) {
$extension = strtolower(pathinfo($raw_path, PATHINFO_EXTENSION));
if (!in_array($extension, $good_extensions)) {
$this->warn('Potentially unsafe file ' . $raw_path . ' is being skipped');
$boring_files[] = $raw_path;
continue 2;
}
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $file - last_pos+strlen(\$file) is: ".($last_pos+strlen($file))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
//no wildcards found in $file, process 'normally'
if ($last_pos + strlen($file) == strlen($raw_path) || $has_wildcard) { //again, no trailing slash. or this is a wildcard and we just take it.
// print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
$interesting_files[$raw_path] = ['dest' => dirname($file), 'index' => $i];
continue 2;
}
}
}
$boring_files[] = $raw_path; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file
} // end of pre-processing the ZIP file for-loop
// print_r($interesting_files);exit(-1);
if (count($sqlfiles) != 1) {
return $this->error('There should be exactly *one* sql backup file found, found: '.(count($sqlfiles) == 0 ? 'None' : implode(', ', $sqlfiles)));
}
if (strpos($sqlfiles[0], 'db-dumps') === false) {
//return $this->error("SQL backup file is missing 'db-dumps' component of full pathname: ".$sqlfiles[0]);
//older Snipe-IT installs don't have the db-dumps subdirectory component
}
$sql_stat = $za->statIndex($sqlfile_indices[0]);
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
$sql_contents = $za->getStream($sql_stat['name']); // maybe copy *THIS* thing?
// OKAY, now that we *found* the sql file if we're doing just the guess-prefix thing, we can do that *HERE* I think?
if ($this->option('sanitize-guess-prefix')) {
$prefix = SQLStreamer::guess_prefix($sql_contents);
$this->line($prefix);
return $this->info("Re-run this command with '--sanitize-with-prefix=".$prefix."' to see an attempt to sanitze your SQL.");
}
//how to invoke the restore?
$pipes = [];
$env_vars = getenv();
$env_vars['MYSQL_PWD'] = config('database.connections.mysql.password');
// TODO notes: we are stealing the dump_binary_path (which *probably* also has your copy of the mysql binary in it. But it might not, so we might need to extend this)
// we unilaterally prepend a slash to the `mysql` command. This might mean your path could look like /blah/blah/blah//mysql - which should be fine. But maybe in some environments it isn't?
$mysql_binary = config('database.connections.mysql.dump.dump_binary_path').\DIRECTORY_SEPARATOR.'mysql'.(\DIRECTORY_SEPARATOR == '\\' ? ".exe" : "");
if( ! file_exists($mysql_binary) ) {
return $this->error("mysql tool at: '$mysql_binary' does not exist, cannot restore. Please edit DB_DUMP_PATH in your .env to point to a directory that contains the mysqldump and mysql binary");
}
$proc_results = proc_open("$mysql_binary -h ".escapeshellarg(config('database.connections.mysql.host')).' -u '.escapeshellarg(config('database.connections.mysql.username')).' '.escapeshellarg(config('database.connections.mysql.database')), // yanked -p since we pass via ENV
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
null,
$env_vars); // this is not super-duper awesome-secure, but definitely more secure than showing it on the CLI, or dropping temporary files with passwords in them.
if ($proc_results === false) {
return $this->error('Unable to invoke mysql via CLI');
}
// I'm not sure about these?
stream_set_blocking($pipes[1], false); // use non-blocking reads for stdout
stream_set_blocking($pipes[2], false); // use non-blocking reads for stderr
// $this->info("Stdout says? ".fgets($pipes[1])); //FIXME: I think we might need to set non-blocking mode to use this properly?
// $this->info("Stderr says? ".fgets($pipes[2])); //FIXME: ditto, same.
// should we read stdout?
// fwrite($pipes[0],config("database.connections.mysql.password")."\n"); //this doesn't work :(
//$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy!
// FIXME - this feels like it wants to go somewhere else?
// and it doesn't seem 'right' - if you can't get a stream to the .sql file,
// why do we care what's happening with pipes and stdout and stderr?!
if ($sql_contents === false) {
$stdout = fgets($pipes[1]);
$this->info($stdout);
$stderr = fgets($pipes[2]);
$this->info($stderr);
return false;
}
try {
if ( $this->option('sanitize-with-prefix') === null) {
// "Legacy" direct-piping
$bytes_read = 0;
while (($buffer = fgets($sql_contents, SQLStreamer::$buffer_size)) !== false) {
$bytes_read += strlen($buffer);
// Log::debug("Buffer is: '$buffer'");
$bytes_written = fwrite($pipes[0], $buffer);
if ($bytes_written === false) {
throw new Exception("Unable to write to pipe");
}
}
} else {
$sql_importer = new SQLStreamer($sql_contents, $pipes[0], $this->option('sanitize-with-prefix'));
$bytes_read = $sql_importer->line_aware_piping();
}
} catch (\Exception $e) {
Log::error("Error during restore!!!! ".$e->getMessage());
// FIXME - put these back and/or put them in the right places?!
$err_out = fgets($pipes[1]);
$err_err = fgets($pipes[2]);
Log::error("Error OUTPUT: ".$err_out);
$this->info($err_out);
Log::error("Error ERROR : ".$err_err);
$this->error($err_err);
throw $e;
}
if (!feof($sql_contents) || $bytes_read == 0) {
return $this->error("Not at end of file for sql file, or zero bytes read. aborting!");
}
fclose($pipes[0]);
fclose($sql_contents);
$this->line(stream_get_contents($pipes[1]));
fclose($pipes[1]);
$this->error(stream_get_contents($pipes[2]));
fclose($pipes[2]);
//wait, have to do fclose() on all pipes first?
$close_results = proc_close($proc_results);
if ($close_results != 0) {
return $this->error('There may have been a problem with the database import: Error number '.$close_results);
}
//and now copy the files over too (right?)
//FIXME - we don't prune the filesystem space yet!!!!
if ($this->option('no-progress')) {
$bar = null;
} else {
$bar = $this->output->createProgressBar(count($interesting_files));
}
foreach ($interesting_files as $pretty_file_name => $file_details) {
$ugly_file_name = $za->statIndex($file_details['index'])['name'];
$fp = $za->getStream($ugly_file_name);
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
$migrated_file = fopen($file_details['dest'].'/'.basename($pretty_file_name), 'w');
while (($buffer = fgets($fp, SQLStreamer::$buffer_size)) !== false) {
fwrite($migrated_file, $buffer);
}
fclose($migrated_file);
fclose($fp);
//$this->info("Wrote $ugly_file_name to $pretty_file_name");
if ($bar) {
$bar->advance();
}
}
if ($bar) {
$bar->finish();
$this->line('');
} else {
$this->info(count($interesting_files).' files were succesfully transferred');
}
foreach ($boring_files as $boring_file) {
$this->warn($boring_file.' was skipped.');
}
}
}
-157
View File
@@ -1,157 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Setting;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Console\Command;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Encryption\Encrypter;
class RotateAppKey extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:rotate-key
{previous_key? : The previous key to rotate from}
{--emergency : Emergency mode - rotate from .env APP_KEY to newly-generated one, modifying .env}
{--force : Skip interactive confirmation}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rotates APP_KEY to a new value, optionally taking the previous key as an argument';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//make sure they specify only exactly one of --emergency, or a filename. Not neither, and not both.
if ( (!$this->option('emergency') && !$this->argument('previous_key')) || ( $this->option('emergency') && $this->argument('previous_key'))) {
$this->error("Specify only one of --emergency, or an app key value, in order to rotate keys");
return 1;
}
if ( $this->option('emergency') ) {
$msg = "\n****************************************************\nTHIS WILL MODIFY YOUR APP_KEY AND DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND \nRE-ENCRYPT THEM WITH A NEWLY GENERATED KEY. \n\nThere is NO undo. \n\nMake SURE you have a database backup and a backup of your .env generated BEFORE running this command. \n\nIf you do not save the newly generated APP_KEY to your .env in this process, \nyour encrypted data will no longer be decryptable. \n\nAre you SURE you wish to continue, and have confirmed you have a database backup and an .env backup? ";
} else {
$msg = "\n****************************************************\nTHIS WILL DE-CRYPT YOUR ENCRYPTED CUSTOM FIELDS AND RE-ENCRYPT THEM WITH YOUR\nAPP_KEY.\n\nThere is NO undo. \n\nMake SURE you have a database backup BEFORE running this command. \n\nAre you SURE you wish to continue, and have confirmed you have a database backup? ";
}
if ($this->option('force') || $this->confirm($msg)) {
// Get the existing app_key and ciphers
// We put them in a variable since we clear the cache partway through here.
if ($this->option('emergency')) {
$old_app_key = config('app.key');
$cipher = config('app.cipher');
// Generate a new one
Artisan::call('key:generate', ['--show' => true]);
$new_app_key = trim(Artisan::output());
// Clear the config cache
Artisan::call('config:clear');
// Write the new app key to the .env file
$this->writeNewEnvironmentFileWith($new_app_key);
} elseif ($this->argument('previous_key')) {
$old_app_key = $this->argument('previous_key');
$cipher = config('app.cipher'); // just a guess?
$new_app_key = config('app.key');
}
$this->warn('Your app cipher is: ' . $cipher);
$this->warn('Your old APP_KEY is: ' . $old_app_key);
$this->warn('Your new APP_KEY is: ' . $new_app_key);
// Manually create an old encrypter instance using the old app key
// and also create a new encrypter instance so we can re-crypt the field
// using the newly generated app key
$oldEncrypter = new Encrypter(base64_decode(substr($old_app_key, 7)), $cipher);
$newEncrypter = new Encrypter(base64_decode(substr($new_app_key, 7)), $cipher);
$fields = CustomField::where('field_encrypted', '1')->get();
foreach ($fields as $field) {
$assets = Asset::whereNotNull($field->db_column)->get();
foreach ($assets as $asset) {
try {
$asset->{$field->db_column} = $oldEncrypter->decrypt($asset->{$field->db_column});
$this->line('DECRYPTED: ' . $field->db_column);
} catch (DecryptException $e) {
$this->line('Could not decrypt '. $field->db_column.' using "old key" - skipping...');
continue;
} catch (\Exception $e) {
$this->error("Error decrypting ".$field->db_column.", reason: ".$e->getMessage().". Aborting key rotation");
throw $e;
}
$asset->{$field->db_column} = $newEncrypter->encrypt($asset->{$field->db_column});
$this->line('ENCRYPTED: '.$field->db_column);
$asset->save();
}
}
// Handle the LDAP password if one is provided
$setting = Setting::first();
if ($setting->ldap_pword != '') {
try {
$setting->ldap_pword = $oldEncrypter->decrypt($setting->ldap_pword);
$setting->ldap_pword = $newEncrypter->encrypt($setting->ldap_pword);
$setting->save();
$this->warn('LDAP password has been re-encrypted.');
} catch(DecryptException $e) {
$this->warn("Unable to decrypt old LDAP password; skipping");
}
}
} else {
$this->info('This operation has been canceled. No changes have been made.');
}
}
/**
* Write a new environment file with the given key.
*
* @param string $key
* @return void
*/
protected function writeNewEnvironmentFileWith($key)
{
file_put_contents($this->laravel->environmentFilePath(), preg_replace(
$this->keyReplacementPattern(),
'APP_KEY="'.$key.'"',
file_get_contents($this->laravel->environmentFilePath())
));
}
/**
* Get a regex pattern that will match env APP_KEY with any random key.
*
* @return string
*/
protected function keyReplacementPattern()
{
$escaped = '="?'.preg_quote($this->laravel['config']['app.key'], '/').'"?';
return "/^APP_KEY{$escaped}/m";
}
}
@@ -1,44 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\SamlNonce;
class SamlClearExpiredNonces extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'saml:clear_expired_nonces';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clears out expired SAML assertions from the saml_nonces table';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
SamlNonce::where('not_valid_after','<=',now())->delete();
return 0;
}
}
@@ -1,54 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use App\Notifications\CurrentInventory;
use Illuminate\Console\Command;
class SendCurrentInventoryToUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:user-inventory';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This will send users a report of all of the items currently checked out to them.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$users = User::whereNull('deleted_at')->whereNotNull('email')->with('assets', 'accessories', 'licenses')->get();
$count = 0;
foreach ($users as $user) {
if (($user->assets->count() > 0) || ($user->accessories->count() > 0) || ($user->licenses->count() > 0) || ($user->consumables->count() > 0)) {
$count++;
$user->notify((new CurrentInventory($user)));
}
}
$this->info($count.' users notified.');
}
}
@@ -1,72 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\Recipients\AlertRecipient;
use App\Models\Setting;
use App\Notifications\ExpectedCheckinAdminNotification;
use App\Notifications\ExpectedCheckinNotification;
use Carbon\Carbon;
use Illuminate\Console\Command;
class SendExpectedCheckinAlerts extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'snipeit:expected-checkin';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check for overdue or upcoming expected checkins.';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$settings = Setting::getSettings();
$interval = $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
$assets = Asset::whereNull('deleted_at')->DueOrOverdueForCheckin($settings)->orderBy('assets.expected_checkin', 'desc')->get();
$this->info($assets->count().' assets must be checked in on or before '.$interval_date.' is deadline');
foreach ($assets as $asset) {
if ($asset->assignedTo && (isset($asset->assignedTo->email)) && ($asset->assignedTo->email!='') && $asset->checkedOutToUser()) {
$this->info('Sending User ExpectedCheckinNotification to: '.$asset->assignedTo->email);
$asset->assignedTo->notify((new ExpectedCheckinNotification($asset)));
}
}
if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item) {
return new AlertRecipient($item);
});
$this->info('Sending Admin ExpectedCheckinNotification to: '.$settings->alert_email);
\Notification::send($recipients, new ExpectedCheckinAdminNotification($assets));
}
}
}
@@ -1,75 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\License;
use App\Models\Recipients\AlertRecipient;
use App\Models\Setting;
use App\Notifications\ExpiringAssetsNotification;
use App\Notifications\ExpiringLicenseNotification;
use Illuminate\Console\Command;
class SendExpirationAlerts extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'snipeit:expiring-alerts';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check for expiring warrantees and service agreements, and sends out an alert email.';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$settings = Setting::getSettings();
$threshold = $settings->alert_interval;
if (($settings->alert_email != '') && ($settings->alerts_enabled == 1)) {
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item, $key) {
return new AlertRecipient($item);
});
// Expiring Assets
$assets = Asset::getExpiringWarrantee($threshold);
if ($assets->count() > 0) {
$this->info(trans_choice('mail.assets_warrantee_alert', $assets->count(), ['count' => $assets->count(), 'threshold' => $threshold]));
\Notification::send($recipients, new ExpiringAssetsNotification($assets, $threshold));
}
// Expiring licenses
$licenses = License::getExpiringLicenses($threshold);
if ($licenses->count() > 0) {
$this->info(trans_choice('mail.license_expiring_alert', $licenses->count(), ['count' => $licenses->count(), 'threshold' => $threshold]));
\Notification::send($recipients, new ExpiringLicenseNotification($licenses, $threshold));
}
} else {
if ($settings->alert_email == '') {
$this->error('Could not send email. No alert email configured in settings');
} elseif (1 != $settings->alerts_enabled) {
$this->info('Alerts are disabled in the settings. No mail will be sent');
}
}
}
}
@@ -1,65 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Helpers\Helper;
use App\Models\Recipients\AlertRecipient;
use App\Models\Setting;
use App\Notifications\InventoryAlert;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Notification;
class SendInventoryAlerts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:inventory-alerts';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command checks for low inventory, and sends out an alert email.';
/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$settings = Setting::getSettings();
if (($settings->alert_email != '') && ($settings->alerts_enabled == 1)) {
$items = Helper::checkLowInventory();
if (($items) && (count($items) > 0)) {
$this->info(trans_choice('mail.low_inventory_alert', count($items)));
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item, $key) {
return new AlertRecipient($item);
});
\Notification::send($recipients, new InventoryAlert($items, $settings->alert_threshold));
}
} else {
if ($settings->alert_email == '') {
$this->error('Could not send email. No alert email configured in settings');
} elseif (1 != $settings->alerts_enabled) {
$this->info('Alerts are disabled in the settings. No mail will be sent');
}
}
}
}
@@ -1,67 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\Recipients\AlertRecipient;
use App\Models\Setting;
use App\Notifications\SendUpcomingAuditNotification;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Console\Command;
class SendUpcomingAuditReport extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:upcoming-audits';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send email/slack notifications for upcoming asset audits.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$settings = Setting::getSettings();
$interval = $settings->audit_warning_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval);
$assets = Asset::whereNull('deleted_at')->DueOrOverdueForAudit($settings)->orderBy('assets.next_audit_date', 'desc')->get();
$this->info($assets->count().' assets must be audited in on or before '.$interval_date.' is deadline');
if (($assets) && ($assets->count() > 0) && ($settings->alert_email != '')) {
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item) {
return new AlertRecipient($item);
});
$this->info('Sending Admin SendUpcomingAuditNotification to: '.$settings->alert_email);
\Notification::send($recipients, new SendUpcomingAuditNotification($assets, $settings->audit_warning_days));
}
}
}
@@ -1,78 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class SyncAssetCounters extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:counter-sync';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Syncs checkedout, checked in, and requested counters for assets';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$start = microtime(true);
// We need the whole count of all assets in order to set up the progress bar
$assets_count = Asset::withTrashed()->count();
$bar = $this->output->createProgressBar($assets_count);
$assets = Asset::withCount('checkins as checkins_count', 'checkouts as checkouts_count', 'userRequests as user_requests_count')
->withTrashed()->chunk(100, function ($assets) use ($bar) {
if ($assets->count() > 0) {
foreach ($assets as $asset) {
$asset->checkin_counter = (int) $asset->checkins_count;
$asset->checkout_counter = (int) $asset->checkouts_count;
$asset->requests_counter = (int) $asset->user_requests_count;
$asset->unsetEventDispatcher();
$asset->save();
$bar->advance();
Log::debug('Asset: '.$asset->id.' has '.$asset->checkin_counter.' checkins, '.$asset->checkout_counter.' checkouts, and '.$asset->requests_counter.' requests');
}
} else {
$this->info('No assets to sync');
}
});
$bar->finish();
$time_elapsed_secs = microtime(true) - $start;
$this->info("\nSync of ".$assets_count.' assets executed in '.$time_elapsed_secs.' seconds');
}
}
-138
View File
@@ -1,138 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use Illuminate\Console\Command;
class SyncAssetLocations extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:sync-asset-locations {--output= : info|warn|error|all} ';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This utility will sync the location_id of assets based on current state. It should not normally be needed, but is a safeguard in case we missed something in the Great Migration when flattening the assets to location relationship.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$output['info'] = [];
$output['warn'] = [];
$output['error'] = [];
$total_assets = Asset::whereNull('deleted_at')->get();
$bar = $this->output->createProgressBar(count($total_assets));
// Unassigned
$rtd_assets = Asset::whereNull('assigned_to')->whereNull('deleted_at')->with('defaultLoc')->get();
$output['info'][] = 'There are '.$rtd_assets->count().' unassigned assets.';
foreach ($rtd_assets as $rtd_asset) {
$output['info'][] = 'Setting Unassigned Asset '.$rtd_asset->id.' ('.$rtd_asset->asset_tag.') to location: '.$rtd_asset->rtd_location_id.' because their default location is: '.$rtd_asset->rtd_location_id;
$rtd_asset->location_id = $rtd_asset->rtd_location_id;
$rtd_asset->unsetEventDispatcher();
$rtd_asset->save();
$bar->advance();
}
$assigned_user_assets = Asset::where('assigned_type', \App\Models\User::class)->whereNotNull('assigned_to')->whereNull('deleted_at')->get();
$output['info'][] = 'There are '.$assigned_user_assets->count().' assets checked out to users.';
foreach ($assigned_user_assets as $assigned_user_asset) {
if (($assigned_user_asset->assignedTo) && ($assigned_user_asset->assignedTo->userLoc)) {
$new_location = $assigned_user_asset->assignedTo->userLoc->id;
$output['info'][] = 'Setting User Asset '.$assigned_user_asset->id.' ('.$assigned_user_asset->asset_tag.') to '.$assigned_user_asset->assignedTo->userLoc->name.' which is id: '.$new_location;
} else {
$output['warn'][] = 'Asset '.$assigned_user_asset->id.' ('.$assigned_user_asset->asset_tag.') still has no location! ';
$new_location = $assigned_user_asset->rtd_location_id;
}
$assigned_user_asset->location_id = $new_location;
$assigned_user_asset->unsetEventDispatcher();
$assigned_user_asset->save();
$bar->advance();
}
$assigned_location_assets = Asset::where('assigned_type', \App\Models\Location::class)
->whereNotNull('assigned_to')->whereNull('deleted_at')->get();
$output['info'][] = 'There are '.$assigned_location_assets->count().' assets checked out to locations.';
foreach ($assigned_location_assets as $assigned_location_asset) {
if ($assigned_location_asset->assignedTo) {
$assigned_location_asset->location_id = $assigned_location_asset->assignedTo->id;
$output['info'][] = 'Setting Location Assigned asset '.$assigned_location_asset->id.' ('.$assigned_location_asset->asset_tag.') that is checked out to '.$assigned_location_asset->assignedTo->name.' (#'.$assigned_location_asset->assignedTo->id.') to location: '.$assigned_location_asset->assetLoc()->id;
$assigned_location_asset->unsetEventDispatcher();
$assigned_location_asset->save();
} else {
$output['warn'][] = 'Asset '.$assigned_location_asset->id.' ('.$assigned_location_asset->asset_tag.') did not return a valid associated location - perhaps it was deleted?';
}
$bar->advance();
}
// Assigned to assets
$assigned_asset_assets = Asset::where('assigned_type', \App\Models\Asset::class)
->whereNotNull('assigned_to')->whereNull('deleted_at')->get();
$output['info'][] = 'Asset-assigned assets: '.$assigned_asset_assets->count();
foreach ($assigned_asset_assets as $assigned_asset_asset) {
// Check to make sure there aren't any invalid relationships
if ($assigned_asset_asset->assetLoc()) {
$assigned_asset_asset->location_id = $assigned_asset_asset->assetLoc()->id;
$output['info'][] = 'Setting Asset Assigned asset '.$assigned_asset_asset->assetLoc()->id.' ('.$assigned_asset_asset->asset_tag.') location to: '.$assigned_asset_asset->assetLoc()->id;
$assigned_asset_asset->unsetEventDispatcher();
$assigned_asset_asset->save();
} else {
$output['warn'][] = 'Asset Assigned asset '.$assigned_asset_asset->id.' ('.$assigned_asset_asset->asset_tag.') does not seem to have a valid location';
}
$bar->advance();
}
$unlocated_assets = Asset::whereNull('location_id')->whereNull('deleted_at')->get();
$output['info'][] = 'Assets still without a location: '.$unlocated_assets->count();
foreach ($unlocated_assets as $unlocated_asset) {
$output['warn'][] = 'Asset: '.$unlocated_asset->id.' still has no location. ';
$bar->advance();
}
$bar->finish();
$this->info("\n");
if (($this->option('output') == 'all') || ($this->option('output') == 'info')) {
foreach ($output['info'] as $key => $output_text) {
$this->info($output_text);
}
}
if (($this->option('output') == 'all') || ($this->option('output') == 'warn')) {
foreach ($output['warn'] as $key => $output_text) {
$this->warn($output_text);
}
}
if (($this->option('output') == 'all') || ($this->option('output') == 'error')) {
foreach ($output['error'] as $key => $output_text) {
$this->error($output_text);
}
}
}
}
-54
View File
@@ -1,54 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class SystemBackup extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'snipeit:backup {--filename=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command creates a database dump and zips up all of the uploaded files in the upload directories.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->option('filename')) {
$filename = $this->option('filename');
// Make sure the filename ends in .zip
if (!ends_with($filename, '.zip')) {
$filename = $filename.'.zip';
}
$this->call('backup:run', ['--filename' => $filename]);
} else {
$this->call('backup:run');
}
}
}
@@ -1,76 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\CustomField;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ToggleCustomfieldEncryption extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:customfield-encryption
{fieldname : the db_column_name of the field}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'This command should be used to convert an unencrypted custom field into a custom field and encrypt the associated data in the assets table for that column.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$fieldname = $this->argument('fieldname');
if ($field = CustomField::where('db_column', $fieldname)->first()) {
// If the field is not encrypted, make it encrypted and encrypt the data in the assets table for the
// corresponding field.
DB::transaction(function () use ($field) {
if ($field->field_encrypted == 0) {
$assets = Asset::whereNotNull($field->db_column)->get();
foreach ($assets as $asset) {
$asset->{$field->db_column} = encrypt($asset->{$field->db_column});
$asset->save();
}
$field->field_encrypted = 1;
$field->save();
// This field is already encrypted. Do nothing.
} else {
$this->error('The custom field ' . $field->db_column.' is already encrypted. No action was taken.');
}
});
// No matching column name found
} else {
$this->error('No matching results for unencrypted custom fields with db_column name: ' . $fieldname.'. Please check the fieldname.');
}
}
}
-127
View File
@@ -1,127 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class Version extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'version:update {--branch=master} {--type=patch}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$use_branch = $this->option('branch');
$use_type = $this->option('type');
$git_branch = trim(shell_exec('git rev-parse --abbrev-ref HEAD'));
$build_version = trim(shell_exec('git rev-list --count '.$use_branch));
$versionFile = 'config/version.php';
$full_hash_version = str_replace("\n", '', shell_exec('git describe master --tags'));
$version = explode('-', $full_hash_version);
$app_version = $current_app_version = $version[0];
$hash_version = (array_key_exists('2', $version)) ? $version[2] : '';
$prerelease_version = '';
$this->line('Branch is: '.$use_branch);
$this->line('Type is: '.$use_type);
$this->line('Current version is: '.$full_hash_version);
if (count($version) == 3) {
$this->line('This does not look like an alpha/beta release.');
} else {
if (array_key_exists('3', $version)) {
$this->line('The current version looks like a beta release.');
$prerelease_version = $version[1];
$hash_version = $version[3];
}
}
$app_version_raw = explode('.', $app_version);
$maj = str_replace('v', '', $app_version_raw[0]);
$min = $app_version_raw[1];
$patch = '';
// This is a major release that might not have a third .0
if (array_key_exists(2, $app_version_raw)) {
$patch = $app_version_raw[2];
}
if ($use_type == 'major') {
$app_version = 'v'.($maj + 1).".$min.$patch";
} elseif ($use_type == 'minor') {
$app_version = 'v'."$maj.".($min + 1).".$patch";
} elseif ($use_type == 'pre') {
$pre_raw = str_replace('beta', '', $prerelease_version);
$pre_raw = str_replace('alpha', '', $pre_raw);
$pre_raw = str_ireplace('rc', '', $pre_raw);
$pre_raw = $pre_raw++;
$this->line('Setting the pre-release to '.$prerelease_version.'-'.$pre_raw);
$app_version = 'v'."$maj.".($min + 1).".$patch";
} elseif ($use_type == 'patch') {
$app_version = 'v'."$maj.$min.".($patch + 1);
// If nothing is passed, leave the version as it is, just increment the build
} else {
$app_version = 'v'."$maj.$min.".$patch;
}
// Determine if this tag already exists, or if this prior to a release
$this->line('Running: git rev-parse master '.$current_app_version);
// $pre_release = trim(shell_exec('git rev-parse '.$use_branch.' '.$current_app_version.' 2>&1 1> /dev/null'));
if ($use_branch == 'develop') {
$app_version = $app_version.'-pre';
}
$full_app_version = $app_version.' - build '.$build_version.'-'.$hash_version;
$array = var_export(
[
'app_version' => $app_version,
'full_app_version' => $full_app_version,
'build_version' => $build_version,
'prerelease_version' => $prerelease_version,
'hash_version' => $hash_version,
'full_hash' => $full_hash_version,
'branch' => $git_branch, ],
true
);
// Construct our file content
$content = <<<CON
<?php
return $array;
CON;
// And finally write the file and output the current version
\File::put($versionFile, $content);
$this->info('Setting NEW version: '.$full_app_version.' ('.$git_branch.')');
}
}
-40
View File
@@ -1,40 +0,0 @@
<?php
namespace App\Console;
use App\Console\Commands\ImportLocations;
use App\Console\Commands\ReEncodeCustomFieldNames;
use App\Console\Commands\RestoreDeletedUsers;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('snipeit:inventory-alerts')->daily();
$schedule->command('snipeit:expiring-alerts')->daily();
$schedule->command('snipeit:expected-checkin')->daily();
$schedule->command('snipeit:backup')->weekly();
$schedule->command('backup:clean')->daily();
$schedule->command('snipeit:upcoming-audits')->daily();
$schedule->command('auth:clear-resets')->everyFifteenMinutes();
$schedule->command('saml:clear_expired_nonces')->weekly();
}
/**
* This method is required by Laravel to handle any console routes
* that are defined in routes/console.php.
*/
protected function commands()
{
require base_path('routes/console.php');
$this->load(__DIR__.'/Commands');
}
}
-23
View File
@@ -1,23 +0,0 @@
<?php
namespace App\Events;
use App\Models\CheckoutAcceptance;
use App\Models\Contracts\Acceptable;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CheckoutAccepted
{
use Dispatchable, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(CheckoutAcceptance $acceptance)
{
$this->acceptance = $acceptance;
}
}
-23
View File
@@ -1,23 +0,0 @@
<?php
namespace App\Events;
use App\Models\CheckoutAcceptance;
use App\Models\Contracts\Acceptable;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CheckoutDeclined
{
use Dispatchable, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(CheckoutAcceptance $acceptance)
{
$this->acceptance = $acceptance;
}
}
-34
View File
@@ -1,34 +0,0 @@
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CheckoutableCheckedIn
{
use Dispatchable, SerializesModels;
public $checkoutable;
public $checkedOutTo;
public $checkedInBy;
public $note;
public $action_date; // Date setted in the hardware.checkin view at the checkin_at input, for the action log
public $originalValues;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($checkoutable, $checkedOutTo, User $checkedInBy, $note, $action_date = null, $originalValues = [])
{
$this->checkoutable = $checkoutable;
$this->checkedOutTo = $checkedOutTo;
$this->checkedInBy = $checkedInBy;
$this->note = $note;
$this->action_date = $action_date ?? date('Y-m-d');
$this->originalValues = $originalValues;
}
}
-32
View File
@@ -1,32 +0,0 @@
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CheckoutableCheckedOut
{
use Dispatchable, SerializesModels;
public $checkoutable;
public $checkedOutTo;
public $checkedOutBy;
public $note;
public $originalValues;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($checkoutable, $checkedOutTo, User $checkedOutBy, $note, $originalValues = [])
{
$this->checkoutable = $checkoutable;
$this->checkedOutTo = $checkedOutTo;
$this->checkedOutBy = $checkedOutBy;
$this->note = $note;
$this->originalValues = $originalValues;
}
}
-8
View File
@@ -1,8 +0,0 @@
<?php
namespace App\Events;
abstract class Event
{
//
}
-24
View File
@@ -1,24 +0,0 @@
<?php
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
class UserMerged
{
use Dispatchable, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $from_user, User $to_user, ?User $admin)
{
$this->merged_from = $from_user;
$this->merged_to = $to_user;
$this->admin = $admin;
}
}
-22
View File
@@ -1,22 +0,0 @@
<?php
namespace App\Exceptions;
use Exception;
class CheckoutNotAllowed extends Exception
{
private $errorMessage;
public function __construct($errorMessage = null)
{
$this->errorMessage = $errorMessage;
parent::__construct($errorMessage);
}
public function __toString()
{
return is_null($this->errorMessage) ? 'A checkout is not allowed under these circumstances' : $this->errorMessage;
}
}
-181
View File
@@ -1,181 +0,0 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use App\Helpers\Helper;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\AuthenticationException;
use ArieTimmerman\Laravel\SCIMServer\Exceptions\SCIMException;
use Illuminate\Support\Facades\Log;
use Throwable;
use JsonException;
use Carbon\Exceptions\InvalidFormatException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Auth\Access\AuthorizationException::class,
\Symfony\Component\HttpKernel\Exception\HttpException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Illuminate\Session\TokenMismatchException::class,
\Illuminate\Validation\ValidationException::class,
\Intervention\Image\Exception\NotSupportedException::class,
\League\OAuth2\Server\Exception\OAuthServerException::class,
JsonException::class,
SCIMException::class, //these generally don't need to be reported
InvalidFormatException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Throwable $exception
* @return void
*/
public function report(Throwable $exception)
{
if ($this->shouldReport($exception)) {
if (class_exists(Log::class)) {
Log::error($exception);
}
return parent::report($exception);
}
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse|\Illuminate\Http\Response
*/
public function render($request, Throwable $e)
{
// CSRF token mismatch error
if ($e instanceof \Illuminate\Session\TokenMismatchException) {
return redirect()->back()->with('error', trans('general.token_expired'));
}
// Invalid JSON exception
// TODO: don't understand why we have to do this when we have the invalidJson() method, below, but, well, whatever
if ($e instanceof JsonException) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Invalid JSON'), 422);
}
// Handle SCIM exceptions
if ($e instanceof SCIMException) {
try {
$e->report(); // logs as 'debug', so shouldn't get too noisy
} catch(\Exception $reportException) {
//do nothing
}
return $e->render($request); // ALL SCIMExceptions have the 'render()' method
}
// Handle standard requests that fail because Carbon cannot parse the date on validation (when a submitted date value is definitely not a date)
if ($e instanceof InvalidFormatException) {
return redirect()->back()->withInput()->with('error', trans('validation.date', ['attribute' => 'date']));
}
// Handle API requests that fail
if ($request->ajax() || $request->wantsJson()) {
// Handle API requests that fail because Carbon cannot parse the date on validation (when a submitted date value is definitely not a date)
if ($e instanceof InvalidFormatException) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('validation.date', ['attribute' => 'date'])), 200);
}
// Handle API requests that fail because the model doesn't exist
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
$className = last(explode('\\', $e->getModel()));
return response()->json(Helper::formatStandardApiResponse('error', null, $className . ' not found'), 200);
}
// Handle API requests that fail because of an HTTP status code and return a useful error message
if ($this->isHttpException($e)) {
$statusCode = $e->getStatusCode();
switch ($e->getStatusCode()) {
case '404':
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode . ' endpoint not found'), 404);
case '429':
return response()->json(Helper::formatStandardApiResponse('error', null, 'Too many requests'), 429);
case '405':
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
default:
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode);
}
}
}
if ($this->isHttpException($e) && (isset($statusCode)) && ($statusCode == '404' )) {
return response()->view('layouts/basic', [
'content' => view('errors/404')
],$statusCode);
}
return parent::render($request, $e);
}
/**
* Convert an authentication exception into an unauthenticated response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Auth\AuthenticationException $exception
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthorized or unauthenticated.'], 401);
}
return redirect()->guest('login');
}
protected function invalidJson($request, ValidationException $exception)
{
return response()->json(Helper::formatStandardApiResponse('error', null, $exception->errors()), 200);
}
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
});
}
}
File diff suppressed because it is too large Load Diff
-25
View File
@@ -1,25 +0,0 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\Storage;
class StorageHelper
{
public static function downloader($filename, $disk = 'default')
{
if ($disk == 'default') {
$disk = config('filesystems.default');
}
switch (config("filesystems.disks.$disk.driver")) {
case 'local':
return response()->download(Storage::disk($disk)->path($filename)); //works for PRIVATE or public?!
case 's3':
return redirect()->away(Storage::disk($disk)->temporaryUrl($filename, now()->addMinutes(5))); //works for private or public, I guess?
default:
return Storage::disk($disk)->download($filename);
}
}
}

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