Compare commits

..

320 Commits

Author SHA1 Message Date
snipe
33b59d7bed Bumped version 2019-05-15 16:45:07 -07:00
snipe
888bdbdb68 Added ability to update groups via API
Fixes [ch9139]
2019-05-15 16:39:34 -07:00
snipe
d67c931f6a Import locations from CSV via command line (#7021)
* Added import locations command

* Small fixes to location importer

* Added country, LDAP OU

* Cleaned up comments, added more clarification to what the script does
2019-05-13 02:27:19 -07:00
snipe
dbdc511eff Merge branch 'master' of https://github.com/snipe/snipe-it 2019-05-08 09:23:58 -04:00
snipe
f47b960566 Added API middleware to API routes to enable throttling
TODO: Figure out how to make this costumizable without touching the code
2019-05-08 09:23:54 -04:00
snipe
d016076806 Fixed #6956 - viewKeys policy inconsistent (#7009)
* Fixed #6956 - Added additional gates show showing/hiding license keys

* Modified gate to allow user to see licenses if they can create or edit the license as well
2019-05-08 08:14:49 -04:00
snipe
23fa5d0bf4 Fixed #7003 - crash when warranty months or purchase date is null 2019-05-07 15:33:57 -04:00
Joris van Eijden
486c708911 Leave the activated state for users alone in normal LDAP synchronisation. (#6988) 2019-05-06 09:40:53 -04:00
snipe
e5c2d77c7d Fixes #6204 - added email alerts and web/API access to assets due for audits (#6992)
* Added upcoming audit report

TODO: Fid diff/threshold math

* Added route to list overdue / upcoming assets via API

* Controller/API methods for due/overdue audits

We could probably skip this and just handle it via view in the routes…

* Added query scopes for due and overdue audits

* Added audit due console command to kernel

* Added ability to pass audit specs to main API asset search method

* Added audit presenter

* Added bootstrap-tables presenter formatter to display an audit button

* Added gated sidenav items to left nav

* Added audit due/overdue blades

* Cleanup on audit due/overdue console command

* Added language strings for audit views

* Fixed :threshold placeholder

* Removed unused setting variable

* Fixed next audit date math

* Added scope for both overdue and upcoming

* Derp. Wrong version

* Bumped version

(I will release this version officially tomorrow)
2019-05-05 22:32:52 -04:00
snipe
ce16eae508 Merge branch 'master' of https://github.com/snipe/snipe-it 2019-05-02 15:20:52 -07:00
snipe
dc73dbfbfd Fixed #6911 - note must be a string on license checkin 2019-05-02 15:20:47 -07:00
snipe
dae26e0378 Remove “Imported from LDAP” note override 2019-04-18 17:56:08 -04:00
snipe
1bb1f7342f Fixed #6922 - date_add crashing if EOL is null 2019-04-18 15:49:59 -04:00
snipe
420e8bc85a Allow phone number to be changed in Profile 2019-04-18 14:13:50 -04:00
snipe
a521523d45 Truncate imports table on seed 2019-04-18 14:13:33 -04:00
snipe
25884a893e Bumped version 2019-03-20 02:41:33 -07:00
snipe
d1e9fbfa24 Updated compoer 2019-03-20 02:37:44 -07:00
snipe
da015ec4a8 Fixed #6834 and #6402 - use inline QR code generation for 2FA (#6840)
* Fixed  #6834 and #6402 - use inline QR code generation for

* Update auth controllers to use translations

* Updated composer lock

* Added comments

* Moar comments

* Typo
2019-03-20 01:24:31 -07:00
snipe
1451b4f45d Merge branch 'master' of https://github.com/snipe/snipe-it 2019-03-18 20:51:11 -07:00
snipe
b6da68a69c Bumped version 2019-03-18 20:50:47 -07:00
snipe
dee92cfc6c Fixes XSS vulnerabilities (#6831)
* Properly escape log_meta values

* Vue syntax fix to allow npm run dev to work again

* Janky fix for Select2 bug

* Compiled production assets

* Escape user’s last name in API

* Removed duplicate alertClass

* Compiled production assets
2019-03-18 20:49:32 -07:00
snipe
dec77890bd Merge branch 'master' of https://github.com/snipe/snipe-it 2019-03-18 11:59:48 -07:00
snipe
0e1289f12f Fixes #6821 - fixed 2 fa active for users list (#6822)
* Fixed #6821 - confusing UI for 2FA when 2FA is universally enforced

I also updated the language in the user’s listing table to clarify what “activated” means

* Added login enabled info to user view

* Clarified comments

* Added info about 2FA on user profile

Because why not

* Added nowrap to table, and added 2FA reset for superadmins
2019-03-18 11:59:02 -07:00
snipe
7b33f95e83 Fixes/import permissions mask (#6826)
* Check for empty headers in import

* Added import permission

* Fixed model path in docblock

* Added import gate to default blade

* Check if the user is an admin OR idf they have import permissions

* Walked back that admin permission

Since admins are bound by full company support, it makes less sense to let admins have this permission by default, versus having them specifically designated to the import permission
2019-03-18 11:58:08 -07:00
snipe
ab6744dfba Merge branch 'master' of https://github.com/snipe/snipe-it 2019-03-14 15:38:20 -07:00
snipe
0fd940ffa4 Check for empty headers in import 2019-03-14 15:38:07 -07:00
Tim Farmer
5893e25b43 Label bugs in alerts settings (#6808)
* Label bugs in alerts settings

Found another label bug where the alert_email label should be alerts_enabled.

* Update alerts.blade.php
2019-03-14 10:53:05 -07:00
snipe
7c3bbe3097 Fixes #6776 hungary date format (#6823)
* Add @timothyfarmer as a contributor

* Fixed #6776 - added hungary time format
2019-03-13 19:39:38 -07:00
snipe
858d382e26 Changed logging to info level for LDAP 2019-03-13 15:14:03 -07:00
snipe
de16fee00a Change image unlink error log to info from error 2019-03-13 12:22:12 -07:00
Tim Farmer
7deab0f53b Label bugs in general settings (#6801)
* checkbox label bug

The checkbox label in general settings for require_accept_signature was set to full_multiple_companies_support instead so when you click the label for require accept signature it would check the wrong box.

* Update general.blade.php

* Update general.blade.php

Another label that is wrong in general settings
2019-03-08 12:24:24 -08:00
Jonathon Reinhart
e59ec8b27f Run Laravel schedule in docker image using supervisord (#6606)
* docker: Rename /entrypoint.sh to /startup.sh

This script is not configured as the docker image ENTRYPOINT, thus it is
misleading to name it so.

* docker: Terminate supervisord if a process enters the FATAL state

By terminating PID 1, this will also terminate the Docker container.

* docker: Use supervisord to start up apache and cron

Note that this uses `apache2ctl -DFOREGROUND` rather than manually
sourcing /etc/apache2/envvars and running apache2, as recommended at
https://advancedweb.hu/2018/07/03/supervisor_docker/.

* docker: Add artisan schedule:run to crontab

This also switches to executing /var/www/html/artisan directly.

* docker: Run artisan schedule:run directly from supervisor

This has the following benefits over using cron:
- Cron doesn't need to be installed
- Docker-provided environment variables are preserved
- It's easy and explicit to run as the docker user
2019-03-07 12:42:00 -08:00
snipe
6d98bd6846 Fixed error if item requested or request was deleted (#6786)
ch628
2019-03-05 23:47:36 -08:00
snipe
58768e5aee Added ability to search consumables by item number (#6785)
Fixes ch1086
2019-03-05 23:21:22 -08:00
snipe
28a450ea25 Added ability to do full name search in user dropdown selectlist (#6784) 2019-03-05 21:13:39 -08:00
snipe
1393f44070 Create FUNDING.yml 2019-03-04 23:57:17 -08:00
snipe
8016939f31 Merge branch 'master' of https://github.com/snipe/snipe-it 2019-03-01 17:39:55 -08:00
snipe
c1ad2f9376 Added link to Marksman - A Windows agent for Snipe-IT
Per https://github.com/Scope-IT/marksman/issues/9#issuecomment-468275142
2019-03-01 17:39:50 -08:00
snipe
9575cd2651 Add accessories endpoint to user API (#6775) 2019-03-01 17:21:03 -08:00
snipe
cf086b711e Merge branch 'master' of https://github.com/snipe/snipe-it 2019-02-22 13:24:58 -08:00
snipe
53db96edad Bumped minor version 2019-02-22 13:24:45 -08:00
snipe
3b62c4a83a Fixes/integrity constraint (#6754)
* Migration to fix nullables

This should fix an issue introduced in 90cddb7aee where we’re passing null instead of an empty string (necessary to nullify values via the API)

* Removed asset migration - serial was already fixed
2019-02-22 13:20:42 -08:00
snipe
5f3147cf36 Added support for enum that was added :( 2019-02-20 21:04:03 -08:00
snipe
738896bdc2 Make serial nullable
Guessing this is new due to later versions of mysql
2019-02-20 20:47:14 -08:00
snipe
e2834fab90 Bumped minor version 2019-02-16 11:48:20 -08:00
snipe
d687e1d762 Fixed #6725 - revert auro_increment_prefix to string
This was mis-migrated as boolean.

It’s essentially a duplicate migration, as it is fixed in-place on the existing migration, and then a new migratio was created to handle anyone who already upgraded.
2019-02-16 11:37:05 -08:00
snipe
6256abddf2 Bumped point release 2019-02-14 15:07:11 -08:00
snipe
b26fbf986f Fixed issue where offset could be greater than total items, resulting in “No results” confusion 2019-02-14 14:49:08 -08:00
snipe
5c9b1ed43a Fixed #6676 - consumables API not respecting category id 2019-02-14 14:48:43 -08:00
snipe
14eb6b387b Possible fix for #6710 - explicitly make auto_increment_prefix nullable (#6716)
* Possible fix for #6710 - explicitly make auto_increment_prefix nullable

* Added migration to make auto_increment_prefix explicitly nullable
2019-02-14 12:46:09 -08:00
snipe
35ebe33e4e Fixed #6703 - fixes password confirmation (#6711)
* Fixed #6703 - fixes password confirmation

* Removed debugging

* Fixed tests

* I guess we use 10 as the settings for password min in tests

* One more try to fix tests - confirmation won’t validate until password validates
2019-02-13 23:01:19 -08:00
snipe
9035707bd6 Bumped point version 2019-02-13 07:40:52 -08:00
snipe
aa1e06f021 One more time…. Fixed #6704 - don’t apply gate to $arrays collection, just check that they can view assets 2019-02-13 04:46:19 -08:00
snipe
30b1cfabf5 Fixed dumb formatting 2019-02-13 04:45:21 -08:00
snipe
e75d22ab73 Revert "Fixed #6704 - don’t apply gate to $arrays collection, just check that they can view assets"
This reverts commit b1e17743b8.
2019-02-13 04:44:19 -08:00
snipe
b1e17743b8 Fixed #6704 - don’t apply gate to $arrays collection, just check that they can view assets 2019-02-13 04:35:55 -08:00
snipe
e2c0f01a10 Fixed #6367 - pass table name and column_id to scopeCompanyables
Solves error: Integrity constraint violation: 1052 Column 'company_id' in where clause is ambiguous
2019-02-13 01:26:11 -08:00
snipe
f88fee0f21 Make user notes field editable via API 2019-02-12 23:58:30 -08:00
snipe
c0669150fb Bumped point version 2019-02-12 23:48:31 -08:00
snipe
f3c12f38b6 Fixed #6061 - Assigned user group cannot be removed
This bug was a result of attempting to check if the groups field had a value, and only THEN trying to sync the groups. This meant that uf you were removing ALL groups, the  sync wouldn’t be triggered.

This still needs to be updated in the API.
2019-02-12 23:43:38 -08:00
snipe
5e19178a30 Do not count deleted locations in managedLocation check on user delete 2019-02-12 23:32:10 -08:00
snipe
90cddb7aee Fixed #6113 - use $asset->fill vs filled() to allow blanking values via API (#6693)
Need to confirm that re-enabling `\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,` won’t mangle anything. I know we ran into some issues when testing a long time ago, but not sure those issues apply anymore, and I can’t remember what they were.
2019-02-12 22:08:38 -08:00
snipe
6d828964be Merge branch 'master' of https://github.com/snipe/snipe-it 2019-02-04 18:58:32 -08:00
snipe
971fcf5800 Fixed #6633 - return 200 status code 2019-02-04 18:58:28 -08:00
Daniel Ruf
2ad270cf33 ci: fix indentation (#6669) 2019-01-30 15:14:17 -08:00
Daniel Ruf
8ce78c6b31 tests: allow to fail on PHP 7.3 and use the latest PHP 7.1 release (#6666) 2019-01-30 14:32:33 -08:00
Daniel Ruf
af3c8195af tests: fix expected string (#6665) 2019-01-30 14:12:20 -08:00
snipe
117b4c59cc Bumped minor version 2019-01-25 21:06:08 -08:00
snipe
194d0733d4 Fixed #6644 - asset name not linked in Reports > Asset Maintenance Report
This report will likely be deprecated.
2019-01-25 20:57:14 -08:00
snipe
a371e8d53f Added calibration as a maintenance type
Should just make these custmizable options
2019-01-24 15:17:33 -08:00
snipe
8f09cca043 Fixed incorrect group route 2019-01-24 15:17:11 -08:00
snipe
39bca49e8f Specify table name in deleted user display 2019-01-24 14:38:18 -08:00
snipe
b8269020ae Specify table name in deleted user display 2019-01-24 14:37:39 -08:00
snipe
601c129bbf Embed images in emails 2019-01-17 20:45:24 -08:00
snipe
b293d00699 Switch LDAP error to debug, to avoid crapping up the logs 2019-01-17 20:18:03 -08:00
snipe
75a0cf97e2 Return an error if asset maintenance is associated with a non-existant asset 2019-01-16 02:19:57 -08:00
snipe
c055e3af21 Only try to return the asset tag link if a valid asset id has been passed 2019-01-16 02:19:35 -08:00
snipe
a1f93e733c Fixed undefined error when maintenance is associated with a deleted asset 2019-01-16 01:45:51 -08:00
fanta8897
49073742b5 Updating LDAP such that each user is not required to be bindable to LDAP (#6571)
* Update Ldap.php

* Update Ldap.php

* Update Ldap.php

* Update Ldap.php

* Update Ldap.php

Updating LDAP.php such that the admin bind will ONLY occur if the user attempting auth cannot bind. If that is the case, it will attempt to bind as admin and search for that user, prior to failing.
2019-01-15 14:04:21 -08:00
Sxderp
187206cb88 Fix saving of REMOTE_USER setting broken by 1a64879b6 (#6565)
The previous commit made it such that remote user login could only
be enabled if two factor authentication was also enabled. Unnest
the configuration so that the setting can be applied without.
2019-01-15 13:59:36 -08:00
Hubert
8420cb7ec1 Fixed problem with import when using snipeit:import command (#6550) 2019-01-15 13:58:23 -08:00
Andrey Bolonin
75252bce05 add php 7.3 (#6556) 2019-01-10 13:21:04 -08:00
snipe
794824713e Bumped minor version 2019-01-03 11:22:06 -08:00
snipe
8f6ea84fca Fixed #4568 - escaping values in custom report 2018-12-12 19:38:24 -08:00
snipe
ea1b792a93 Fixed #6491 - cleaner return methods for PHP 7.3 compact() 2018-12-12 18:23:39 -08:00
Ben RUBSON
4ffb8f14b8 Improve Memcached settings (#6485)
* Improved memcached settings

* Improved memcached settings
2018-12-06 14:39:14 -08:00
Wes Hulette
d023f61bc4 Fixed missing importer (#6413)
Fixed missing manager_id
Fixed missing department_id
2018-12-04 13:06:12 -08:00
snipe
dd5ca73602 Set support footer and version footer to on when resetting the demo 2018-11-08 12:52:37 -08:00
snipe
2632f730d1 Sets activated to 0 in UserImporter if the activated column isn’t set 2018-11-07 22:36:58 -08:00
snipe
24c158bfe6 Added missing use statement for departments in importer 2018-11-07 18:33:43 -08:00
snipe
3d4a5a8066 More importer tweaks for dept and manager 2018-11-07 18:05:53 -08:00
snipe
db7e0b56f2 Fixed department id on asset import with users 2018-11-07 17:36:34 -08:00
snipe
f2478d813c Fix manager id if no manager is given in importer 2018-11-07 17:33:27 -08:00
snipe
192aa9eb71 Fixed #6386 - licenses not searching on category name 2018-11-02 17:14:08 -07:00
Tim Bishop
0eef0fc1dd Sync with develop branch. (#6377)
Without this change argv[1] is ignored.
2018-10-31 11:11:41 -07:00
snipe
81f8fe34cd Removed debugging line 2018-10-30 18:11:27 -07:00
snipe
f744696043 Fixed #6375 - lowercase keys on findAndBindUser to address LDAP syncing issue 2018-10-30 13:12:10 -07:00
snipe
29b0780c6c Added company info to asset maintenances transforrmer 2018-10-30 00:20:16 -07:00
Wes Hulette
6b3b673daa Changed NULL coalesce from ?? (#6353)
PHP 5 does not have the double question mark null coalesce support.
2018-10-26 15:53:18 -07:00
snipe
a4876e9f3e Updated language files 2018-10-26 15:51:38 -07:00
snipe
925258bfb4 Bumped version 2018-10-26 15:29:10 -07:00
snipe
1a10aa0dda Fixed #6349 - add view permission for print all assigned 2018-10-19 16:43:28 -07:00
snipe
d6f8d1b464 Updated composer lock 2018-10-19 16:40:54 -07:00
snipe
09a102fea8 Only try to return a department if there is a matching field 2018-10-19 01:44:45 -07:00
snipe
304fce73fc Null if blank on user import 2018-10-19 01:38:14 -07:00
snipe
295a68bb7a Try false instead of null 2018-10-19 01:36:15 -07:00
snipe
3aeb521782 Patch PR #6335 to master 2018-10-19 01:30:05 -07:00
snipe
f587d2248b WIP: Better handle activation column in importer (#6290)
* Better handle activation column

* Added comments for clarity on importer methods
2018-10-19 00:23:12 -07:00
snipe
8579c5a68a Allow 0 as a consumable min amt 2018-10-15 17:04:51 -07:00
snipe
835b461d7d Fixed #6323 - typo in link for low inventory 2018-10-11 15:31:10 -07:00
Wes Hulette
b8a37a0c73 Fixed Expiring Assets Email (#6321) 2018-10-11 14:03:00 -07:00
snipe
41b5b1dfd0 Bumped hash 2018-10-09 17:32:46 -07:00
snipe
c7596e7741 Fixed image not uploading on asset create 2018-10-09 17:31:52 -07:00
snipe
d4fa81301d Check if user can see assets in statuslabels gate 2018-10-09 16:34:12 -07:00
snipe
ec7245965f Bumped to rollbar 2.4.1
https://github.com/rollbar/rollbar-php-laravel/issues/65
2018-10-04 17:09:12 -07:00
snipe
de76e8db5f Re-enable rollbar 2018-10-04 12:11:36 -07:00
snipe
a52575c7bf Lock rollbar to v2.3.0
https://github.com/rollbar/rollbar-php-laravel/issues/65
https://github.com/rollbar/rollbar-php-laravel/issues/67
2018-10-04 12:11:36 -07:00
Nenad Ticaric
bf6703c2e8 fixing double word typo (#6292)
Thanks!
2018-10-04 09:41:50 -07:00
snipe
4db1dd8afc Fixed #6291 - send-welcome argument in cli importer 2018-10-04 04:43:06 -07:00
snipe
c39e3acb59 Bumped hash 2018-10-04 02:24:41 -07:00
snipe
7a44da85a0 Fixed issue where admin users could disable activation when editing their own profile 2018-10-04 02:18:20 -07:00
snipe
890b613f71 Temporarily suppress rollbar 2018-10-03 15:47:56 -07:00
snipe
1014bd74e0 Updated rollbar 2018-10-03 14:58:14 -07:00
snipe
db385e024b Possible proxy issue fix 2018-10-03 13:04:25 -07:00
snipe
c8bff3ef38 Features/add manager and dept to importer (#6277)
* Ignore the simlink for public storage

* Added manager and department to user import

* More UI importer tweaks

* Fisxed typos
2018-10-02 15:43:54 -07:00
snipe
10bc35d604 Fixed PHP warning Undefined offset: 1 in upgrade.php 2018-09-28 12:13:35 -07:00
snipe
eea65a3f26 Fixed manufacturers item count 2018-09-28 12:03:27 -07:00
snipe
3b21a19491 Updated language strings 2018-09-28 11:54:52 -07:00
snipe
7a52477294 Added Icelandic and Serbian to locale list 2018-09-28 11:47:59 -07:00
snipe
ef0bd72076 Bumped version 2018-09-28 11:21:39 -07:00
snipe
ff824ec4db Fixes #6252 - activated flag not checked when editing active user 2018-09-28 11:18:33 -07:00
snipe
75032def9e Fixed #4151 - Undefined index: samaccountname on LDAP import 2018-09-27 16:11:09 -07:00
snipe
3a0f738fb0 Added some hepful comments 2018-09-26 19:20:50 -07:00
snipe
55846cc717 Changed LOG:: to Log:: 2018-09-26 19:06:31 -07:00
snipe
1784278a59 Fixed importer email test 2018-09-26 19:06:09 -07:00
snipe
afac0bc441 Removed old isActive model (unused) 2018-09-26 19:05:42 -07:00
snipe
ffbee77f6f Patch for 5965 - multiple email recipients no longer working (#6238) 2018-09-26 15:47:53 -07:00
snipe
b69b5fdf84 Added counts to location show() API method 2018-09-21 15:50:14 -07:00
Brady Wetherington
89e06054bf Merge pull request #6205 from dasjoe/patch-1
Fixes #5630
2018-09-17 21:45:48 -07:00
Hajo Möller
3159e7713a Fixes #5630 2018-09-11 13:07:45 +02:00
snipe
adf6e7d1cd Added group support for user API 2018-09-07 18:25:58 -07:00
snipe
ba3662a9ed Bumped hash 2018-09-07 18:12:29 -07:00
snipe
05ea61421f Added manager_id to fillable for locations API 2018-09-07 18:11:38 -07:00
snipe
22ef2ce0b6 Bumped hash 2018-09-07 03:20:31 -07:00
snipe
51d3d130e4 Fixed Not unique table/alias: 'models' on custom report triggered when category is selected 2018-09-07 03:19:54 -07:00
snipe
d8a8e1cc09 Bumped version 2018-08-28 11:43:56 -07:00
snipe
522dc1db2a Fixed #6124 2018-08-23 21:05:10 -07:00
snipe
db907815ff Removed check for active in password reset form 2018-08-21 18:40:27 -07:00
snipe
ae6abdddad Check the user is active before displaying password reset
This would only come into play if an inactive user already received a password reset email and then the system was upgraded to prevent those emails from being sent to inactive users
2018-08-14 19:04:47 -07:00
snipe
63c9fbe10c Temporarily disbable notifying users on import 2018-08-14 18:27:46 -07:00
snipe
101dfd01f2 Bumped hash 2018-08-14 18:22:53 -07:00
snipe
5db5134ae0 Set activated to default on when new user is created 2018-08-14 18:14:41 -07:00
snipe
5294489b0e Fixed HTML typo 2018-08-14 18:14:29 -07:00
snipe
05b2b8fb59 Tweaked code/language for password reset 2018-08-14 18:09:33 -07:00
snipe
0100c56046 Only allow password reset if user is active 2018-08-14 17:46:29 -07:00
Jordi Boggiano
e81b221fd1 Fix license identifier (#6043) 2018-08-08 20:36:45 -07:00
snipe
f374ac1bf7 Removed duplicate BACKUP_ENV in example env 2018-08-01 15:31:43 -07:00
snipe
524c6c502e Features/restore deleted cmd (#5982)
* Delete content from login attempts table

* Script to restore deleted users and put their asset assignments back

* Uncomment backup
2018-07-27 02:42:55 -07:00
snipe
614e858e44 Restrict users asset listing to just assets checked out to users 2018-07-25 21:38:14 -07:00
snipe
708b1a962c Split out custom_css from custom_header
This makes it so that the custom_css will still be respected even if there is no custom header
2018-07-25 19:01:47 -07:00
snipe
4e55a18a60 Bumped version 2018-07-24 19:46:23 -07:00
snipe
3de1de9dc6 Merge branch 'develop' 2018-07-24 14:04:16 -07:00
snipe
e320d2ba05 Fixed #5944 - added logo option for print-assets page (#5950) 2018-07-24 13:37:02 -07:00
snipe
ed78a4b8a0 Fixed activated issue for strict mode 2018-07-24 13:28:59 -07:00
snipe
376eb52f00 Fixed #5938 - added “self location edit” as permission 2018-07-24 12:42:16 -07:00
snipe
8ecceeacda Fixed weird display for self options 2018-07-24 12:41:58 -07:00
snipe
f4cfb31bf4 Use request object 2018-07-24 12:10:02 -07:00
snipe
227dc7e81d Save model display setting - fix for issue in #5301 2018-07-24 12:10:02 -07:00
snipe
152d985ebc Add @Azerothian as a contributor 2018-07-24 12:10:02 -07:00
Earl Ramirez
ef1e8df001 Disable file browsing (#5922)
* Added cron to list of packages

* Disable file browsing from the public directory
2018-07-23 20:40:04 -07:00
snipe
5c2b1a3b70 Add @Azerothian as a contributor 2018-07-23 20:29:15 -07:00
Azerothian
66c3f5432d implemented specific seat checkout (#5887) 2018-07-23 20:28:45 -07:00
Daniel Meltzer
de413408f5 Port/reenable most unit tests. (#5921)
* Port/reenable most unit tests.

Should probably flesh out notifications tests in the next few days.

* Disable json checkin in ApiAssetsTest@index for now.  It's broken, but hiding other real broken things.

* Re Disable Groups allowDelete
2018-07-23 06:48:21 -07:00
Daniel Meltzer
059126f642 Checkout update locationid (#5919)
* Fix missing punctuation.  Bad merge.

* If we're checking out to an location, use it's id instead of location_id
2018-07-23 06:47:21 -07:00
Daniel Meltzer
3bc43210ab Add ID to the allowed sort fields in api/Users. (#5929) 2018-07-23 06:46:50 -07:00
Daniel Meltzer
82194cef8a bugfix: updating a user when an admin (not a superuser) would remove any groups from the user. (#5914) 2018-07-21 23:02:06 -07:00
snipe
1956a16d1e Merge branch 'develop' 2018-07-20 13:25:03 -07:00
Till Deeke
e1c095adca Removes the typehint for search term string (#5904)
The „string“ typehint only works in PHP >= 7.0.0.
Since we are still supporting versions below that, remove the type hint.
2018-07-20 13:23:44 -07:00
Till Deeke
45a2932f4b Fixes the generation of where conditions (#5902) 2018-07-20 13:23:29 -07:00
Till Deeke
b6e3715cd8 Fix: No Notifications for checking out Consumables (#5898)
* Adds a method to consumables to check if a notification should be sent

Adds the checkin_email method to Consumables, this gets checked in notifications when checking out the consumable.

Without the method, no notifications get sent for checking out consumables.

* Fixes the checkin_email method on the License model

This should allow the License to also send checkout/checkin notifications again.
2018-07-20 13:22:49 -07:00
snipe
aa9c0078a1 Merge branch 'develop'
# Conflicts:
#	config/version.php
2018-07-19 14:47:27 -07:00
snipe
9677115055 Bumped version 2018-07-19 14:46:08 -07:00
snipe
d45e90e358 One more fix for #5893 2018-07-19 14:45:28 -07:00
snipe
f692fb5bff Merge branch 'develop'
# Conflicts:
#	config/version.php
2018-07-19 10:50:18 -07:00
snipe
83692696ba Bumped version 2018-07-19 10:49:40 -07:00
snipe
bbb15d610f Merge branch 'develop' 2018-07-19 10:40:38 -07:00
snipe
7ebb7876c4 Partial fix for #5896
Still need to fix the front end on edit, which seems to be defaulting to boolean
2018-07-19 10:40:07 -07:00
snipe
bd581d01a3 Use full row width for image field 2018-07-19 10:40:07 -07:00
snipe
de2ebba577 Category add/edit UI tweaks 2018-07-19 10:40:07 -07:00
Daniel Meltzer
351274c633 Hotfix: the checkin_email does not exist on consumable. (#5891) 2018-07-19 10:38:50 -07:00
snipe
53fab3e9ba Merge branch 'develop' 2018-07-19 10:23:18 -07:00
snipe
a0c0b7b1eb Fixed #5893 - activated typo 2018-07-19 10:22:08 -07:00
snipe
5a34d43a86 Fixed #5895 - wrong date validation in maintenances 2018-07-19 10:19:55 -07:00
snipe
86a0f77e6f Merge branch 'develop' 2018-07-19 10:15:38 -07:00
snipe
9d00ae6e50 Fixed #5894 - lookup by asset tag in top search broken 2018-07-19 10:14:02 -07:00
snipe
8d23398176 Merge branch 'develop'
# Conflicts:
#	config/version.php
2018-07-18 20:07:14 -07:00
snipe
5aae930e8c Bumped version 2018-07-18 20:05:48 -07:00
snipe
6db096c336 Add @takuy as a contributor 2018-07-18 20:04:09 -07:00
snipe
78d27e82c8 Updated language strings 2018-07-18 20:03:31 -07:00
Sam
2a4fef6a61 adds select-id as an option for the asset-select partial. allows you to manually override the ID of an element. currently, the bulk-checkout partial has two asset-selects, with conflicting IDs. attempts to resolve #5882 (#5883) 2018-07-18 19:55:13 -07:00
snipe
9daeeeb851 Features/nicer notifications (#5886)
* Improved expiring licenses notification

* Improved expiring assets notification

* Nicee low inventory notification

* Refactored stupid language strings

* Oops

* Use settings variable
2018-07-18 19:15:45 -07:00
Daniel Meltzer
92671823d8 Userimport fixes/improvements (#5884)
* Send notification when user is created.
* Flesh out default user mappings
* Add user importing test.
2018-07-18 19:15:07 -07:00
Daniel Meltzer
19396b2107 Logic Fix (#5877)
If we have a username, we should look that up even if we do not have a
first name.
2018-07-18 19:10:36 -07:00
Brady Wetherington
f0332a7388 Add DB port number to docker
which gets copied to the docker image's `.env` file during installation. This closes #5718
2018-07-18 19:10:06 -07:00
snipe
70cb5ed215 Updated language strings 2018-07-18 08:55:38 -07:00
snipe
d309f67df0 Set activated to zero if no values passed for active user 2018-07-18 08:27:26 -07:00
snipe
7dc070ec2c Add @engrzhou as a contributor 2018-07-18 07:39:46 -07:00
snipe
2ecdf19569 Add @GodUseVPN as a contributor 2018-07-18 07:39:23 -07:00
snipe
c56f2625b7 Add @grayhoax as a contributor 2018-07-18 07:38:57 -07:00
snipe
3dc154991a Add @wira-sandy as a contributor 2018-07-18 07:37:56 -07:00
snipe
a5f57c050f Add @vipsystem as a contributor 2018-07-18 07:37:30 -07:00
snipe
36c4cd98e0 Add @vinzruzell as a contributor 2018-07-18 07:36:31 -07:00
snipe
f6ef6039f4 Add @fraccie as a contributor 2018-07-18 07:35:55 -07:00
snipe
6867563e4a Add @dheche as a contributor 2018-07-18 07:33:19 -07:00
snipe
21820262ce Add @reinvanhaaren as a contributor 2018-07-18 07:29:12 -07:00
snipe
bae1203b64 Add @ragnarcx as a contributor 2018-07-18 07:28:41 -07:00
snipe
0f2ff7aba2 Add @ProfFan as a contributor 2018-07-18 07:28:03 -07:00
snipe
0ce90834f6 Add @priatna as a contributor 2018-07-18 07:27:42 -07:00
snipe
0e182bb2b9 Add @bodrovics as a contributor 2018-07-18 07:26:54 -07:00
snipe
7afff69fab Add @pawel1615 as a contributor 2018-07-18 07:26:12 -07:00
snipe
136d59e4b9 Add @drcryo as a contributor 2018-07-18 07:25:49 -07:00
snipe
6d9cbac928 Add @pooot as a contributor 2018-07-18 07:25:25 -07:00
snipe
efcd8d339b Add @saymd as a contributor 2018-07-18 07:24:51 -07:00
snipe
7e09ce468b Add @omego as a contributor 2018-07-18 07:24:08 -07:00
snipe
baf714b41f Add @MohammedFota as a contributor 2018-07-18 07:22:56 -07:00
snipe
b0808e846d Add @IxFail as a contributor 2018-07-18 07:22:15 -07:00
snipe
0e3e10a707 Add @mikaelssen as a contributor 2018-07-18 07:21:32 -07:00
snipe
cbf3d5d071 Add @micaelrodrigues as a contributor 2018-07-18 07:20:20 -07:00
snipe
68d355a33d Add @meyerf99 as a contributor 2018-07-18 07:19:59 -07:00
snipe
853923013a Add @stubben as a contributor 2018-07-18 07:19:00 -07:00
snipe
737061a983 Add @msjohansen as a contributor 2018-07-18 07:17:34 -07:00
snipe
0438d55284 Add @mariejoyacajes as a contributor 2018-07-18 07:17:08 -07:00
snipe
924962c156 Add @MarcosBL as a contributor 2018-07-18 07:16:30 -07:00
snipe
a8c42843f6 Add @lstrojny as a contributor 2018-07-18 07:15:10 -07:00
snipe
63c4b7b6b6 Add @laopangzi as a contributor 2018-07-18 07:14:47 -07:00
snipe
ff06387d90 Add @joxelito94 as a contributor 2018-07-18 07:11:31 -07:00
snipe
fa46622bad Add @JohnWillker as a contributor 2018-07-18 07:07:13 -07:00
snipe
fd65780287 Add @jarby1211 as a contributor 2018-07-18 07:05:34 -07:00
snipe
6478ec86ac Add @itangiang as a contributor 2018-07-18 07:04:23 -07:00
snipe
748e0eb44f Add @igolman as a contributor 2018-07-18 07:03:30 -07:00
snipe
507c07e9eb Add @abaalkh as a contributor 2018-07-18 07:03:09 -07:00
snipe
5185a9e590 Add @husnulyaqien as a contributor 2018-07-18 07:02:34 -07:00
snipe
1ade088a8f Add @Kentsson as a contributor 2018-07-18 07:00:50 -07:00
snipe
5c087181be Add @fofwisdom as a contributor 2018-07-18 07:00:21 -07:00
snipe
08bb314bbc Add @Hafidzi as a contributor 2018-07-18 06:59:43 -07:00
snipe
7ddb9becd4 Add @AdnanAbuShahad as a contributor 2018-07-18 06:59:03 -07:00
snipe
7d96709ee2 Add @mrgluek as a contributor 2018-07-18 06:58:28 -07:00
snipe
4413a9f0b6 Add @jgroblesr85 as a contributor 2018-07-18 06:58:10 -07:00
snipe
9611b55fb9 Add @georgwallisch as a contributor 2018-07-18 06:57:39 -07:00
snipe
dffd535f68 Add @gdraque as a contributor 2018-07-18 06:57:17 -07:00
snipe
4aaf2da651 Add @possebon as a contributor 2018-07-18 06:55:47 -07:00
snipe
80ac46af78 Add @fgbs as a contributor 2018-07-18 06:55:12 -07:00
snipe
13c58b54fa Add @frapposelli as a contributor 2018-07-18 06:54:39 -07:00
snipe
355119187a Add @Erlpil as a contributor 2018-07-18 06:54:03 -07:00
snipe
0d96587b04 Add @EpixFr as a contributor 2018-07-18 06:53:30 -07:00
snipe
be806def91 Add @dominiksenti as a contributor 2018-07-18 06:50:59 -07:00
snipe
8acf8027e9 Add @danielcb as a contributor 2018-07-18 06:49:25 -07:00
snipe
893a1fec57 Add @danielheene as a contributor 2018-07-18 06:48:58 -07:00
snipe
d182e40aef Add @da-friedl as a contributor 2018-07-18 06:48:34 -07:00
snipe
6118aca949 Add @dpyroc as a contributor 2018-07-18 06:48:11 -07:00
snipe
f0ac83179f Add @Wxcafe as a contributor 2018-07-18 06:47:18 -07:00
snipe
8969ffc14b Update @CronKz as a contributor 2018-07-18 06:46:45 -07:00
snipe
9fb130146a Add @kopi-item as a contributor 2018-07-18 06:46:24 -07:00
snipe
d285ff673a Add @Againstreality as a contributor 2018-07-18 06:46:00 -07:00
snipe
7f586da856 Add @cwlin0416 as a contributor 2018-07-18 06:45:24 -07:00
snipe
c35f5b3116 Add @chibacityblues as a contributor 2018-07-18 06:44:40 -07:00
snipe
12927d2d54 Add @rudashi as a contributor 2018-07-18 06:43:43 -07:00
snipe
6b06b87547 Add @benunter as a contributor 2018-07-18 06:42:30 -07:00
snipe
058f8ae3b8 Add @aschiavon91 as a contributor 2018-07-18 06:40:14 -07:00
snipe
1faedac04b Add @angeldeejay as a contributor 2018-07-18 06:39:39 -07:00
snipe
666c7321af Add @xelan as a contributor 2018-07-18 06:39:05 -07:00
snipe
6cbaf7396a Add @sirrus as a contributor 2018-07-18 06:38:25 -07:00
snipe
b706acaa9f Add @RealEnder as a contributor 2018-07-18 06:36:42 -07:00
snipe
5870acb193 Add @albertoaldrigo as a contributor 2018-07-18 06:36:04 -07:00
snipe
c00b633312 Add @a-royal as a contributor 2018-07-18 06:33:05 -07:00
snipe
1c0ee7c4c5 Fixed license checkout gate 2018-07-18 05:33:14 -07:00
snipe
961741b809 Fixed license checkout gate 2018-07-18 05:31:59 -07:00
snipe
5a66f0f373 Include show_in_list option in select 2018-07-18 04:00:04 -07:00
snipe
5a1e1c73c9 Include show_in_list option in select 2018-07-18 03:59:02 -07:00
snipe
3be68ec721 Fix location edit permissions 2018-07-18 03:43:45 -07:00
snipe
0f81e494ee Fix location update permission 2018-07-18 03:42:43 -07:00
snipe
d88f66b019 Updated to stale.yml 2018-07-18 02:57:05 -07:00
snipe
027edbdb21 Fixed #5872 - asset maintenances listing showing created_at instead of start_date 2018-07-18 02:24:53 -07:00
Daniel Meltzer
cf03d25934 Fix importer emailformat (#5871)
* Fix Importer emailformat

Str::slug() strips periods from the string, which caused our existing
logic to misbehave when generating a user's email on an import.  Adjust
logic to use generateEmail() helper on user instead.  Also clean up some
of the logic in this method.

* Remove dead code.

* More refactor/cleanup of the user create method.  I think it is almost readable now.
2018-07-17 16:46:08 -07:00
snipe
75232d2a70 Added link to jamf2snipe 2018-07-17 14:17:13 -07:00
snipe
bcb966af12 Updated composer 2018-07-17 14:17:02 -07:00
snipe
0272e58868 Add @DeusMaximus as a contributor 2018-07-17 01:11:39 -07:00
snipe
bcd988bb81 Merge branch 'develop' of https://github.com/DeusMaximus/snipe-it into develop
# Conflicts:
#	app/Http/Controllers/Auth/LoginController.php
2018-07-17 01:11:15 -07:00
snipe
aa6c21f38d Fixed typo 2018-07-17 01:03:32 -07:00
snipe
3fb5f6f5be Revert 2018-07-17 01:03:25 -07:00
snipe
5dc2ac9e22 Try to fix test
This is a listing, not a single asset
2018-07-17 00:20:06 -07:00
snipe
0f85d6810b Added login log 2018-07-16 23:49:08 -07:00
snipe
bf761946da Fix activated check for login 2018-07-16 23:48:46 -07:00
snipe
d9fa2f0e91 Fixed #5842 - added components to location detail view 2018-07-16 21:50:14 -07:00
snipe
8a25677a8d Bumped hash 2018-07-16 21:35:21 -07:00
DeusMaximus
7c2da81700 Fix REMOTE_USER Header with IIS and AD
Remove DOMAIN\ portion of DOMAIN\user when using Windows Authentication and IIS with REMOTE_USER.
2018-07-17 14:03:19 +10:00
snipe
a4799a495a Fixes #5859 - add file name/size to file upload UI (#5861)
* Fixes #5859 - add file name/size to file upload UI

* Reverting assetcontroller

Not sure exactly what happened here…

* Production assets
2018-07-16 20:09:53 -07:00
Till Deeke
b5de5ac19c Fix: Searching for multiple terms on assets (#5860)
* Give advancedTextSearch all search terms at one

The additional conditions for assets had some problems, since they were joining tables for the additional attributes. The method was called once for every search term, so the join was added multiple times if the user entered multiple search terms.

* Allows search to handle multiple search terms better

The search now better handles multiple search terms, adding additional orWhere clauses, instead of duplicating all queries.

* Fixing typo
2018-07-16 17:44:31 -07:00
Daniel Meltzer
638a7b2d91 Assetcontroller cleanup (#5858)
* Extract method/cleanup

* Remove apiStore method that is unusued since api controllers.

* Use proper model exception

* Remove old user importer.  This is now supported by the general importer framework.

* Refactor AssetsController methods.

This is a giant diff without many functional changes, mostly cosmetic.
I've pulled a number of methods out of assetscontroller, preferring
instead to create some more targetted controllers for related actions.
I think this cleans up the file some, and suggests some places for
future targetted improvement.

Fix weird missing things.

* Fix Unit test failing after date changes.

* Pass valid string to be translated.

* Some method cleanup for codacy.

* Extract trait for common checkout uses and codacy fixes.
2018-07-16 17:44:03 -07:00
Daniel Meltzer
dcf72ce2da Strip timestamps from comparison for api tests, in an attempt to fix the test failures. (#5847) 2018-07-16 14:41:51 -07:00
Daniel Meltzer
8f6e0ad5be Only error if checking out to asset with same id (#5845)
A user with an id of 2 is perfectly fine as a checkout_target of an asset with an id of 2.
2018-07-16 14:25:36 -07:00
Daniel Meltzer
50e0b9b84e category_id not category_i (#5844) 2018-07-16 14:23:24 -07:00
Daniel Meltzer
b6b93550fe Remove old helpers (#5843)
* Cleanup model bulk-edit

Use the general partials where appropriate, as well as display a list of
what models we are editing in the bulk edit.

* Use new api based fetch/display for modal select2.

This is just copy/pasting the code currently because I'm not entirely
sure how the two pieces of code interact.

* Remove old helper functions that are no longer necessary with our populating of select2 dropdowns via ajax.
2018-07-16 14:22:25 -07:00
Till Deeke
baa3be728d Refactoring: A nicer and easier syntax for searching models (#5841)
* Adds the ability to search by dates

Adding extra „where“-conditions to the „TextSearch“ queries, allowing the users to search by dates

* Adds missing dates to $dates in models

* Removes duplicated „where“ conditions

* Adds the Searchable trait to models, defining the searchable attributes and relations

* Removes the old text search methods

* Adds back additional conditions to the search

These conditions could not be modeled in the „attributes“ or „relations“, so we include them here

* Removes unnecessary check for the deleted_at attribute

* Fixes typo in comments

* suppresses errors from Codacy

We can safely ignore the error codacy is throwing here, since this method is a standin/noop for models who need to implement more advanced searches
2018-07-16 14:13:07 -07:00
Till Deeke
240e642fe9 Removes the unused bulk operations for components (#5840) 2018-07-16 14:11:38 -07:00
Till Deeke
07a92d20d7 Fixing #5773: Refactoring the "clearing" of select2 lists (#5839)
* adds select2 placeholders to select lists

To allow us to clear the selection on „select2“ selects, we need a placeholder attribute

See: https://select2.org/placeholders

* Removes empty option from multiple select

select2 requires an empty option value on singular selects, but not on multiple selects.

When selecting multiple options, this empty option would be shown as selectable otherwise, not clearing the selection.

* Adds the option to clear select2 instances

Sets the correct options to allow clearing of out select2 instances. The empty placeholder is required, since clearing only works when a placeholder ist set (event an empty one).

See: https://select2.org/placeholders

* Removes the „Clear selection“ option from select lists

Since we can clear the select2 lists with their native clearing method, we can remove this hack

* Updates generated assets (css/js)
2018-07-16 14:10:54 -07:00
Till Deeke
3f334406d1 Fixing #5470: Checkin emails not working (#5838)
* Always send checkin notifications to users

This fixes the routing of the notifications, to only send „checkin“ emails if the „mail on checkin“ flag on the category was set. (and we checkout to a user with a non-empty email)

* Fixes checkout notification routing

Notifications to users should be send if the category of the resource (accessory/asset/consumable/license):

a) requires the user to confirm acceptance
b) should send notifications on checkin/checkout

* adds a check for EULAs

Adds back a check for the EULA, since the user should receive the EULA if it was set (regardless of other setings on the category, etc)
2018-07-16 14:09:04 -07:00
Till Deeke
dbd177576e Brings back the „pending“ state when checking out (#5837)
When a user would get an asset checked out for them, and the assets category required acceptance of the asset, the „pending“ state would not get set.
2018-07-16 14:07:24 -07:00
Till Deeke
0fb9f42ba4 Removes setting the encryption status on update (#5833)
When we are updating a custom field, we don’t want to change the „field_encrypted“-setting on it.
2018-07-13 04:04:30 -07:00
snipe
b4542d4d42 Fix migration for models on labels 2018-07-12 21:49:44 -07:00
snipe
4bfd7a7e4e Add @chemfy as a contributor 2018-07-12 18:28:43 -07:00
snipe
3fe1562b92 Add @jasonlshelton as a contributor 2018-07-12 18:28:43 -07:00
Till Deeke
27699aa99c Adds permission checks for custom fields and custom fieldsets (#5645) (#5795)
* adds permission checks to custom fields

* adds permission checks to custom fieldsets

* adds separate permissions for custom fieldsets

* check for permissions in views

* Removes custom fieldsets from permissions config

* Proxy the authorization for custom fieldsets down to custom fields.

This allows us to use the existing permissions in use and have more semantically correct authorization checks for custom fieldsets.

* simplifies the authorization check for the custom fields overview

* removes special handling of custom fieldsets in base policy

I just realised that this code duplicates the logic from the custom fieldset policy.
Since we are checking for the authorization of custom fields anyway, we can just use the columnName for the fields.

* cleanup of unused imports
2018-07-12 18:28:20 -07:00
Till Deeke
48bbbe0f40 Fixing authorization issues (#5807)
* adds permission checks for companies

* adds permission checks for depreciations

* adds permission check for all reports

* fixes permissions for departments

* fixes permission naming (edit -> update)

* fixes authorization checking wrong permission in API

The authorization was checking for the non-existent „edit“ method where it should have checked for the „update“ method.

* adds authorization checks for select2 lists

* adds missing authorization checks for api

* fixes user authorization check for creating users

* adds additional check viewing assets on showing a users assets

* Removes authorization checks for select2 lists

Reference: https://github.com/snipe/snipe-it/pull/5807#pullrequestreview-136018755
2018-07-12 18:28:02 -07:00
Antti
9dc226e3d6 Feature: PostgreSQL support (#5642)
* Made migrations work with pgsql and changing empty integers to null

* Fixed the last functional test
2018-07-12 18:24:43 -07:00
Jason
98b20fc1cd Added option to include model information on asset labels. (#5301)
* Added option to include model information on asset labels.

Cleaned up label page to fix skewed label alignment on last row per page.

* Changes made per Snipe's direction

changed type from tinyint to boolean in DB
changed labels back to initials
2018-07-12 18:23:12 -07:00
snipe
980dccf31c Add @5quirrel as a contributor 2018-07-12 18:19:35 -07:00
snipe
bb2193d481 Add @tilldeeke as a contributor 2018-07-12 18:19:14 -07:00
5quirrel
bf8fe316df Fix for #4901 (#5829) 2018-07-12 16:45:12 -07:00
snipe
96716626c6 Fixed #5828 - typo 2018-07-12 14:07:07 -07:00
885 changed files with 24150 additions and 6415 deletions

View File

@@ -863,7 +863,8 @@
"avatar_url": "https://avatars0.githubusercontent.com/u/34064225?v=4",
"profile": "https://github.com/CronKz",
"contributions": [
"code"
"code",
"translation"
]
},
{
@@ -974,6 +975,690 @@
"contributions": [
"code"
]
},
{
"login": "tilldeeke",
"name": "Till Deeke",
"avatar_url": "https://avatars2.githubusercontent.com/u/1795149?v=4",
"profile": "https://tilldeeke.de",
"contributions": [
"code"
]
},
{
"login": "5quirrel",
"name": "5quirrel",
"avatar_url": "https://avatars0.githubusercontent.com/u/12634129?v=4",
"profile": "https://github.com/5quirrel",
"contributions": [
"code"
]
},
{
"login": "jasonlshelton",
"name": "Jason",
"avatar_url": "https://avatars1.githubusercontent.com/u/13071957?v=4",
"profile": "https://github.com/jasonlshelton",
"contributions": [
"code"
]
},
{
"login": "chemfy",
"name": "Antti",
"avatar_url": "https://avatars3.githubusercontent.com/u/7128321?v=4",
"profile": "https://github.com/chemfy",
"contributions": [
"code"
]
},
{
"login": "DeusMaximus",
"name": "DeusMaximus",
"avatar_url": "https://avatars3.githubusercontent.com/u/10080364?v=4",
"profile": "https://github.com/DeusMaximus",
"contributions": [
"code"
]
},
{
"login": "A-ROYAL",
"name": "a-royal",
"avatar_url": "https://avatars2.githubusercontent.com/u/16384611?v=4",
"profile": "https://github.com/A-ROYAL",
"contributions": [
"translation"
]
},
{
"login": "albertoaldrigo",
"name": "Alberto Aldrigo",
"avatar_url": "https://avatars0.githubusercontent.com/u/5358208?v=4",
"profile": "https://github.com/albertoaldrigo",
"contributions": [
"translation"
]
},
{
"login": "RealEnder",
"name": "Alex Stanev",
"avatar_url": "https://avatars0.githubusercontent.com/u/1412342?v=4",
"profile": "http://alex.stanev.org/blog",
"contributions": [
"translation"
]
},
{
"login": "sirrus",
"name": "Andreas Rehm",
"avatar_url": "https://avatars0.githubusercontent.com/u/177295?v=4",
"profile": "http://devel.itsolution2.de",
"contributions": [
"translation"
]
},
{
"login": "xelan",
"name": "Andreas Erhard",
"avatar_url": "https://avatars0.githubusercontent.com/u/5080535?v=4",
"profile": "https://github.com/xelan",
"contributions": [
"translation"
]
},
{
"login": "angeldeejay",
"name": "Andrés Vanegas Jiménez",
"avatar_url": "https://avatars2.githubusercontent.com/u/142350?v=4",
"profile": "https://github.com/angeldeejay",
"contributions": [
"translation"
]
},
{
"login": "aschiavon91",
"name": "Antonio Schiavon",
"avatar_url": "https://avatars0.githubusercontent.com/u/3910403?v=4",
"profile": "https://github.com/aschiavon91",
"contributions": [
"translation"
]
},
{
"login": "benunter",
"name": "benunter",
"avatar_url": "https://avatars0.githubusercontent.com/u/10464547?v=4",
"profile": "https://github.com/benunter",
"contributions": [
"translation"
]
},
{
"login": "rudashi",
"name": "Borys Żmuda",
"avatar_url": "https://avatars1.githubusercontent.com/u/5038647?v=4",
"profile": "http://catweb24.pl",
"contributions": [
"translation"
]
},
{
"login": "chibacityblues",
"name": "chibacityblues",
"avatar_url": "https://avatars0.githubusercontent.com/u/5539359?v=4",
"profile": "https://github.com/chibacityblues",
"contributions": [
"translation"
]
},
{
"login": "cwlin0416",
"name": "Chien Wei Lin",
"avatar_url": "https://avatars1.githubusercontent.com/u/1954830?v=4",
"profile": "https://github.com/cwlin0416",
"contributions": [
"translation"
]
},
{
"login": "Againstreality",
"name": "Christian Schuster",
"avatar_url": "https://avatars3.githubusercontent.com/u/11700533?v=4",
"profile": "https://github.com/Againstreality",
"contributions": [
"translation"
]
},
{
"login": "kopi-item",
"name": "Christian Stefanus",
"avatar_url": "https://avatars1.githubusercontent.com/u/4308704?v=4",
"profile": "http://chriss.webhostid.com",
"contributions": [
"translation"
]
},
{
"login": "wxcafe",
"name": "wxcafé",
"avatar_url": "https://avatars3.githubusercontent.com/u/3009327?v=4",
"profile": "http://wxcafe.net",
"contributions": [
"translation"
]
},
{
"login": "dpyroc",
"name": "dpyroc",
"avatar_url": "https://avatars3.githubusercontent.com/u/35761525?v=4",
"profile": "https://github.com/dpyroc",
"contributions": [
"translation"
]
},
{
"login": "da-friedl",
"name": "Daniel Friedlmaier",
"avatar_url": "https://avatars1.githubusercontent.com/u/2153639?v=4",
"profile": "http://www.friedlmaier.net",
"contributions": [
"translation"
]
},
{
"login": "danielheene",
"name": "Daniel Heene",
"avatar_url": "https://avatars1.githubusercontent.com/u/2947640?v=4",
"profile": "https://github.com/danielheene",
"contributions": [
"translation"
]
},
{
"login": "danielcb",
"name": "danielcb",
"avatar_url": "https://avatars3.githubusercontent.com/u/319022?v=4",
"profile": "https://github.com/danielcb",
"contributions": [
"translation"
]
},
{
"login": "dominiksenti",
"name": "Dominik Senti",
"avatar_url": "https://avatars3.githubusercontent.com/u/15846537?v=4",
"profile": "https://github.com/dominiksenti",
"contributions": [
"translation"
]
},
{
"login": "EpixFr",
"name": "Eric Gautheron",
"avatar_url": "https://avatars0.githubusercontent.com/u/25570954?v=4",
"profile": "http://www.konectik.com",
"contributions": [
"translation"
]
},
{
"login": "Erlpil",
"name": "Erlend Pilø",
"avatar_url": "https://avatars1.githubusercontent.com/u/5732623?v=4",
"profile": "https://erlpil.com",
"contributions": [
"translation"
]
},
{
"login": "frapposelli",
"name": "Fabio Rapposelli",
"avatar_url": "https://avatars0.githubusercontent.com/u/541832?v=4",
"profile": "http://fabio.technology",
"contributions": [
"translation"
]
},
{
"login": "fgbs",
"name": "Felipe Barros",
"avatar_url": "https://avatars2.githubusercontent.com/u/3605240?v=4",
"profile": "https://github.com/fgbs",
"contributions": [
"translation"
]
},
{
"login": "possebon",
"name": "Fernando Possebon",
"avatar_url": "https://avatars0.githubusercontent.com/u/257745?v=4",
"profile": "https://github.com/possebon",
"contributions": [
"translation"
]
},
{
"login": "gdraque",
"name": "gdraque",
"avatar_url": "https://avatars3.githubusercontent.com/u/2540832?v=4",
"profile": "https://github.com/gdraque",
"contributions": [
"translation"
]
},
{
"login": "georgwallisch",
"name": "Georg Wallisch",
"avatar_url": "https://avatars0.githubusercontent.com/u/23440381?v=4",
"profile": "https://github.com/georgwallisch",
"contributions": [
"translation"
]
},
{
"login": "jgroblesr85",
"name": "Gerardo Robles",
"avatar_url": "https://avatars1.githubusercontent.com/u/9852832?v=4",
"profile": "https://github.com/jgroblesr85",
"contributions": [
"translation"
]
},
{
"login": "mrgluek",
"name": "Gluek",
"avatar_url": "https://avatars2.githubusercontent.com/u/11082640?v=4",
"profile": "https://t.me/Gluek",
"contributions": [
"translation"
]
},
{
"login": "AdnanAbuShahad",
"name": "AdnanAbuShahad",
"avatar_url": "https://avatars0.githubusercontent.com/u/6847946?v=4",
"profile": "https://github.com/AdnanAbuShahad",
"contributions": [
"translation"
]
},
{
"login": "hafidzi",
"name": "Hafidzi My",
"avatar_url": "https://avatars1.githubusercontent.com/u/3580608?v=4",
"profile": "https://hafidzi.my",
"contributions": [
"translation"
]
},
{
"login": "fofwisdom",
"name": "Harim Park",
"avatar_url": "https://avatars2.githubusercontent.com/u/205521?v=4",
"profile": "https://github.com/fofwisdom",
"contributions": [
"translation"
]
},
{
"login": "Kentsson",
"name": "Henrik Kentsson",
"avatar_url": "https://avatars2.githubusercontent.com/u/3333841?v=4",
"profile": "http://www.kentsson.se",
"contributions": [
"translation"
]
},
{
"login": "husnulyaqien",
"name": "Husnul Yaqien",
"avatar_url": "https://avatars0.githubusercontent.com/u/36551034?v=4",
"profile": "https://github.com/husnulyaqien",
"contributions": [
"translation"
]
},
{
"login": "abaalkh",
"name": "Ibrahim",
"avatar_url": "https://avatars1.githubusercontent.com/u/2372747?v=4",
"profile": "http://abaalkhail.org",
"contributions": [
"translation"
]
},
{
"login": "igolman",
"name": "igolman",
"avatar_url": "https://avatars0.githubusercontent.com/u/1389334?v=4",
"profile": "https://github.com/igolman",
"contributions": [
"translation"
]
},
{
"login": "itangiang",
"name": "itangiang",
"avatar_url": "https://avatars1.githubusercontent.com/u/3257070?v=4",
"profile": "https://github.com/itangiang",
"contributions": [
"translation"
]
},
{
"login": "jarby1211",
"name": "jarby1211",
"avatar_url": "https://avatars2.githubusercontent.com/u/14814254?v=4",
"profile": "https://github.com/jarby1211",
"contributions": [
"translation"
]
},
{
"login": "JohnWillker",
"name": "Jhonn Willker",
"avatar_url": "https://avatars3.githubusercontent.com/u/6719357?v=4",
"profile": "http://jwillker.com",
"contributions": [
"translation"
]
},
{
"login": "joxelito94",
"name": "Jose",
"avatar_url": "https://avatars2.githubusercontent.com/u/10983635?v=4",
"profile": "https://github.com/joxelito94",
"contributions": [
"translation"
]
},
{
"login": "laopangzi",
"name": "laopangzi",
"avatar_url": "https://avatars0.githubusercontent.com/u/5206122?v=4",
"profile": "https://github.com/laopangzi",
"contributions": [
"translation"
]
},
{
"login": "lstrojny",
"name": "Lars Strojny",
"avatar_url": "https://avatars2.githubusercontent.com/u/79707?v=4",
"profile": "http://usrportage.de",
"contributions": [
"translation"
]
},
{
"login": "MarcosBL",
"name": "MarcosBL",
"avatar_url": "https://avatars0.githubusercontent.com/u/389801?v=4",
"profile": "http://twitter.com/marcosbl",
"contributions": [
"translation"
]
},
{
"login": "mariejoyacajes",
"name": "marie joy cajes",
"avatar_url": "https://avatars3.githubusercontent.com/u/35664606?v=4",
"profile": "https://github.com/mariejoyacajes",
"contributions": [
"translation"
]
},
{
"login": "msjohansen",
"name": "Mark S. Johansen",
"avatar_url": "https://avatars2.githubusercontent.com/u/3052816?v=4",
"profile": "http://www.markjohansen.dk",
"contributions": [
"translation"
]
},
{
"login": "stubben",
"name": "Martin Stub",
"avatar_url": "https://avatars2.githubusercontent.com/u/982885?v=4",
"profile": "http://martinstub.dk",
"contributions": [
"translation"
]
},
{
"login": "meyerf99",
"name": "Meyer Flavio",
"avatar_url": "https://avatars2.githubusercontent.com/u/28959963?v=4",
"profile": "https://github.com/meyerf99",
"contributions": [
"translation"
]
},
{
"login": "MicaelRodrigues",
"name": "Micael Rodrigues",
"avatar_url": "https://avatars3.githubusercontent.com/u/796443?v=4",
"profile": "https://github.com/MicaelRodrigues",
"contributions": [
"translation"
]
},
{
"login": "mikaelssen",
"name": "Mikael Rasmussen",
"avatar_url": "https://avatars0.githubusercontent.com/u/10481331?v=4",
"profile": "http://rubixy.com/",
"contributions": [
"translation"
]
},
{
"login": "IxFail",
"name": "IxFail",
"avatar_url": "https://avatars1.githubusercontent.com/u/1544552?v=4",
"profile": "https://github.com/IxFail",
"contributions": [
"translation"
]
},
{
"login": "MohammedFota",
"name": "Mohammed Fota",
"avatar_url": "https://avatars3.githubusercontent.com/u/18483118?v=4",
"profile": "http://www.mohammedfota.com",
"contributions": [
"translation"
]
},
{
"login": "omego",
"name": "Moayad Alserihi",
"avatar_url": "https://avatars0.githubusercontent.com/u/227080?v=4",
"profile": "https://github.com/omego",
"contributions": [
"translation"
]
},
{
"login": "saymd",
"name": "saymd",
"avatar_url": "https://avatars0.githubusercontent.com/u/1680266?v=4",
"profile": "https://github.com/saymd",
"contributions": [
"translation"
]
},
{
"login": "pooot",
"name": "Patrik Larsson",
"avatar_url": "https://avatars0.githubusercontent.com/u/1826808?v=4",
"profile": "https://nordsken.se",
"contributions": [
"translation"
]
},
{
"login": "drcryo",
"name": "drcryo",
"avatar_url": "https://avatars1.githubusercontent.com/u/20584746?v=4",
"profile": "https://github.com/drcryo",
"contributions": [
"translation"
]
},
{
"login": "pawel1615",
"name": "pawel1615",
"avatar_url": "https://avatars1.githubusercontent.com/u/19408004?v=4",
"profile": "https://github.com/pawel1615",
"contributions": [
"translation"
]
},
{
"login": "bodrovics",
"name": "bodrovics",
"avatar_url": "https://avatars2.githubusercontent.com/u/23340468?v=4",
"profile": "https://github.com/bodrovics",
"contributions": [
"translation"
]
},
{
"login": "priatna",
"name": "priatna",
"avatar_url": "https://avatars0.githubusercontent.com/u/3257654?v=4",
"profile": "https://github.com/priatna",
"contributions": [
"translation"
]
},
{
"login": "ProfFan",
"name": "Fan Jiang",
"avatar_url": "https://avatars1.githubusercontent.com/u/5358374?v=4",
"profile": "https://amayume.net",
"contributions": [
"translation"
]
},
{
"login": "ragnarcx",
"name": "ragnarcx",
"avatar_url": "https://avatars1.githubusercontent.com/u/22555451?v=4",
"profile": "https://github.com/ragnarcx",
"contributions": [
"translation"
]
},
{
"login": "reinvanhaaren",
"name": "Rein van Haaren",
"avatar_url": "https://avatars2.githubusercontent.com/u/18654582?v=4",
"profile": "http://www.reinvanhaaren.nl/",
"contributions": [
"translation"
]
},
{
"login": "dheche",
"name": "Teguh Dwicaksana",
"avatar_url": "https://avatars1.githubusercontent.com/u/386672?v=4",
"profile": "http://dheche.songolimo.net",
"contributions": [
"translation"
]
},
{
"login": "FRaccie",
"name": "fraccie",
"avatar_url": "https://avatars2.githubusercontent.com/u/2572552?v=4",
"profile": "https://github.com/FRaccie",
"contributions": [
"translation"
]
},
{
"login": "vinzruzell",
"name": "vinzruzell",
"avatar_url": "https://avatars0.githubusercontent.com/u/35182720?v=4",
"profile": "https://github.com/vinzruzell",
"contributions": [
"translation"
]
},
{
"login": "vipsystem",
"name": "Kevin Austin",
"avatar_url": "https://avatars1.githubusercontent.com/u/7883603?v=4",
"profile": "http://kevinaustin.com",
"contributions": [
"translation"
]
},
{
"login": "wira-sandy",
"name": "Wira Sandy",
"avatar_url": "https://avatars3.githubusercontent.com/u/3861828?v=4",
"profile": "http://azuraweb.xyz",
"contributions": [
"translation"
]
},
{
"login": "GrayHoax",
"name": "Илья",
"avatar_url": "https://avatars2.githubusercontent.com/u/8663789?v=4",
"profile": "https://github.com/GrayHoax",
"contributions": [
"translation"
]
},
{
"login": "godusevpn",
"name": "GodUseVPN",
"avatar_url": "https://avatars3.githubusercontent.com/u/30119111?v=4",
"profile": "https://github.com/godusevpn",
"contributions": [
"translation"
]
},
{
"login": "EngrZhou",
"name": "周周",
"avatar_url": "https://avatars1.githubusercontent.com/u/745576?v=4",
"profile": "https://github.com/EngrZhou",
"contributions": [
"translation"
]
},
{
"login": "takuy",
"name": "Sam",
"avatar_url": "https://avatars3.githubusercontent.com/u/1631095?v=4",
"profile": "https://github.com/takuy",
"contributions": [
"code"
]
},
{
"login": "Azerothian",
"name": "Azerothian",
"avatar_url": "https://avatars1.githubusercontent.com/u/264022?v=4",
"profile": "https://www.illisian.com.au",
"contributions": [
"code"
]
},
{
"login": "timothyfarmer",
"name": "Tim Farmer",
"avatar_url": "https://avatars1.githubusercontent.com/u/7632599?v=4",
"profile": "https://github.com/timothyfarmer",
"contributions": [
"code"
]
}
]
}

View File

@@ -7,7 +7,6 @@ APP_KEY=ChangeMe
APP_URL=null
APP_TIMEZONE='UTC'
APP_LOCALE=en
BACKUP_ENV=false
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
@@ -73,13 +72,20 @@ ENABLE_CSP=false
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
# OPTIONAL: REDIS SETTINGS
# --------------------------------------------
REDIS_HOST=null
REDIS_PASSWORD=null
REDIS_PORT-null
REDIS_PORT=null
# --------------------------------------------
# OPTIONAL: MEMCACHED SETTINGS
# --------------------------------------------
MEMCACHED_HOST=null
MEMCACHED_PORT=null
# --------------------------------------------
# OPTIONAL: AWS S3 SETTINGS

5
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
# 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

23
.github/stale.yml vendored
View File

@@ -13,12 +13,31 @@ exemptLabels:
- "👩‍💻 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. Thank you
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: false
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
.gitignore vendored
View File

@@ -50,3 +50,4 @@ tests/_support/_generated/*
/storage/oauth-public.key
*.cache
/public/storage

View File

@@ -16,8 +16,13 @@ services:
php:
- 5.6
- 7.0
- 7.2
- 7.1.4
- 7.1
- 7.2
- 7.3
matrix:
allow_failures:
- php: 7.3
# execute any number of scripts before the test run, custom env's are available as variables
before_script:

View File

@@ -18,8 +18,8 @@ patch \
curl \
vim \
git \
cron \
mysql-client \
supervisor \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
@@ -68,7 +68,9 @@ RUN \
&& rm -r "/var/www/html/storage/app/backups" && ln -fs "/var/lib/snipeit/dumps" "/var/www/html/storage/app/backups" \
&& mkdir "/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 docker "/var/lib/snipeit/keys/"
&& chown docker "/var/lib/snipeit/keys/" \
&& chmod +x /var/www/html/artisan \
&& echo "Finished setting up application in /var/www/html"
############## DEPENDENCIES via COMPOSER ###################
@@ -95,16 +97,11 @@ VOLUME ["/var/lib/snipeit"]
##### START SERVER
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
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
# Add Tini
ENV TINI_VERSION v0.14.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
CMD ["/entrypoint.sh"]
CMD ["/startup.sh"]
EXPOSE 80
EXPOSE 443

View File

@@ -1,5 +1,5 @@
[![Build Status](https://travis-ci.org/snipe/snipe-it.svg?branch=master)](https://travis-ci.org/snipe/snipe-it) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/snipe/snipe-it?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![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://api.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade)
[![All Contributors](https://img.shields.io/badge/all_contributors-105-orange.svg?style=flat-square)](#contributors) [![Open Source Helpers](https://www.codetriage.com/snipe/snipe-it/badges/users.svg)](https://www.codetriage.com/snipe/snipe-it)
[![All Contributors](https://img.shields.io/badge/all_contributors-181-orange.svg?style=flat-square)](#contributors) [![Open Source Helpers](https://www.codetriage.com/snipe/snipe-it/badges/users.svg)](https://www.codetriage.com/snipe/snipe-it)
## Snipe-IT - Open Source Asset Management System
@@ -10,7 +10,7 @@ It is built on [Laravel 5.4](http://laravel.com).
Snipe-IT is actively developed and we're [releasing quite frequently](https://github.com/snipe/snipe-it/releases). ([Check out the live demo here](https://snipeitapp.com/demo/).)
__This is web-based software__. This means there 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, 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 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, 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.
-----
@@ -58,6 +58,8 @@ Since the release of the JSON REST API, several third-party developers have been
- [SnipeSharp - .NET module in C#](https://github.com/barrycarey/SnipeSharp) by [@barrycarey](https://github.com/barrycarey)
- [InQRy](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/ParadoxGuitarist/jamf2snipe) by [@ParadoxGuitarist](https://github.com/ParadoxGuitarist) - Python script to sync assets between a JAMFPro instance and a Snipe-II instance
- [Marksman](https://github.com/Scope-IT/marksman) - A Windows agent for Snipe-IT
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. :)
@@ -82,8 +84,19 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars2.githubusercontent.com/u/16108587?v=3" width="110px;"/><br /><sub>Jason Spriggs</sub>](http://jasonspriggs.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jasonspriggs "Code") | [<img src="https://avatars2.githubusercontent.com/u/1134568?v=3" width="110px;"/><br /><sub>Nate Felton</sub>](http://n8felton.wordpress.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=n8felton "Code") | [<img src="https://avatars2.githubusercontent.com/u/14036694?v=3" width="110px;"/><br /><sub>Manasses Ferreira</sub>](http://homepages.dcc.ufmg.br/~manassesferreira)<br />[💻](https://github.com/snipe/snipe-it/commits?author=manassesferreira "Code") | [<img src="https://avatars0.githubusercontent.com/u/15913949?v=3" width="110px;"/><br /><sub>Steve</sub>](https://github.com/steveelwood)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=steveelwood "Tests") | [<img src="https://avatars1.githubusercontent.com/u/3361683?v=3" width="110px;"/><br /><sub>matc</sub>](http://twitter.com/matc)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=matc "Tests") | [<img src="https://avatars3.githubusercontent.com/u/7405702?v=3" width="110px;"/><br /><sub>Cole R. Davis</sub>](http://www.davisracingteam.com)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=VanillaNinjaD "Tests") | [<img src="https://avatars2.githubusercontent.com/u/10167681?v=3" width="110px;"/><br /><sub>gibsonjoshua55</sub>](https://github.com/gibsonjoshua55)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gibsonjoshua55 "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/2809241?v=4" width="110px;"/><br /><sub>Robin Temme</sub>](https://github.com/zwerch)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zwerch "Code") | [<img src="https://avatars0.githubusercontent.com/u/6961695?v=4" width="110px;"/><br /><sub>Iman</sub>](https://github.com/imanghafoori1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=imanghafoori1 "Code") | [<img src="https://avatars1.githubusercontent.com/u/6551003?v=4" width="110px;"/><br /><sub>Richard Hofman</sub>](https://github.com/richardhofman6)<br />[💻](https://github.com/snipe/snipe-it/commits?author=richardhofman6 "Code") | [<img src="https://avatars0.githubusercontent.com/u/3697569?v=4" width="110px;"/><br /><sub>gizzmojr</sub>](https://github.com/gizzmojr)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gizzmojr "Code") | [<img src="https://avatars3.githubusercontent.com/u/404729?v=4" width="110px;"/><br /><sub>Jenny Li</sub>](https://github.com/imjennyli)<br />[📖](https://github.com/snipe/snipe-it/commits?author=imjennyli "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/869227?v=4" width="110px;"/><br /><sub>Geoff Young</sub>](https://github.com/GeoffYoung)<br />[💻](https://github.com/snipe/snipe-it/commits?author=GeoffYoung "Code") | [<img src="https://avatars3.githubusercontent.com/u/1068477?v=4" width="110px;"/><br /><sub>Elliot Blackburn</sub>](http://www.elliotblackburn.com)<br />[📖](https://github.com/snipe/snipe-it/commits?author=BlueHatbRit "Documentation") |
| [<img src="https://avatars1.githubusercontent.com/u/6357451?v=4" width="110px;"/><br /><sub>Tõnis Ormisson</sub>](http://andmemasin.eu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=TonisOrmisson "Code") | [<img src="https://avatars0.githubusercontent.com/u/449411?v=4" width="110px;"/><br /><sub>Nicolai Essig</sub>](http://www.nicolai-essig.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thakilla "Code") | [<img src="https://avatars1.githubusercontent.com/u/14809698?v=4" width="110px;"/><br /><sub>Danielle</sub>](https://github.com/techincolor)<br />[📖](https://github.com/snipe/snipe-it/commits?author=techincolor "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/18545156?v=4" width="110px;"/><br /><sub>Lawrence</sub>](https://github.com/TheVakman)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=TheVakman "Tests") [🐛](https://github.com/snipe/snipe-it/issues?q=author%3ATheVakman "Bug reports") | [<img src="https://avatars1.githubusercontent.com/u/22473767?v=4" width="110px;"/><br /><sub>uknzaeinozpas</sub>](https://github.com/uknzaeinozpas)<br />[⚠️](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Tests") [💻](https://github.com/snipe/snipe-it/commits?author=uknzaeinozpas "Code") | [<img src="https://avatars3.githubusercontent.com/u/422752?v=4" width="110px;"/><br /><sub>Ryan</sub>](https://github.com/Gelob)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Gelob "Documentation") | [<img src="https://avatars1.githubusercontent.com/u/10672546?v=4" width="110px;"/><br /><sub>vcordes79</sub>](https://github.com/vcordes79)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vcordes79 "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/27958330?v=4" width="110px;"/><br /><sub>fordster78</sub>](https://github.com/fordster78)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fordster78 "Code") | [<img src="https://avatars0.githubusercontent.com/u/34064225?v=4" width="110px;"/><br /><sub>CronKz</sub>](https://github.com/CronKz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CronKz "Code") | [<img src="https://avatars1.githubusercontent.com/u/585486?v=4" width="110px;"/><br /><sub>Tim Bishop</sub>](https://github.com/tdb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tdb "Code") | [<img src="https://avatars2.githubusercontent.com/u/5384694?v=4" width="110px;"/><br /><sub>Sean McIlvenna</sub>](https://www.seanmcilvenna.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanmcilvenna "Code") | [<img src="https://avatars3.githubusercontent.com/u/36515590?v=4" width="110px;"/><br /><sub>cepacs</sub>](https://github.com/cepacs)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Acepacs "Bug reports") [📖](https://github.com/snipe/snipe-it/commits?author=cepacs "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/37537300?v=4" width="110px;"/><br /><sub>lea-mink</sub>](https://github.com/lea-mink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lea-mink "Code") | [<img src="https://avatars0.githubusercontent.com/u/7140719?v=4" width="110px;"/><br /><sub>Hannah Tinkler</sub>](https://github.com/hannahtinkler)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hannahtinkler "Code") |
| [<img src="https://avatars3.githubusercontent.com/u/27958330?v=4" width="110px;"/><br /><sub>fordster78</sub>](https://github.com/fordster78)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fordster78 "Code") | [<img src="https://avatars0.githubusercontent.com/u/34064225?v=4" width="110px;"/><br /><sub>CronKz</sub>](https://github.com/CronKz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=CronKz "Code") [🌍](#translation-CronKz "Translation") | [<img src="https://avatars1.githubusercontent.com/u/585486?v=4" width="110px;"/><br /><sub>Tim Bishop</sub>](https://github.com/tdb)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tdb "Code") | [<img src="https://avatars2.githubusercontent.com/u/5384694?v=4" width="110px;"/><br /><sub>Sean McIlvenna</sub>](https://www.seanmcilvenna.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=seanmcilvenna "Code") | [<img src="https://avatars3.githubusercontent.com/u/36515590?v=4" width="110px;"/><br /><sub>cepacs</sub>](https://github.com/cepacs)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Acepacs "Bug reports") [📖](https://github.com/snipe/snipe-it/commits?author=cepacs "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/37537300?v=4" width="110px;"/><br /><sub>lea-mink</sub>](https://github.com/lea-mink)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lea-mink "Code") | [<img src="https://avatars0.githubusercontent.com/u/7140719?v=4" width="110px;"/><br /><sub>Hannah Tinkler</sub>](https://github.com/hannahtinkler)<br />[💻](https://github.com/snipe/snipe-it/commits?author=hannahtinkler "Code") |
| [<img src="https://avatars1.githubusercontent.com/u/1086388?v=4" width="110px;"/><br /><sub>Doeke Zanstra</sub>](https://github.com/doekman)<br />[💻](https://github.com/snipe/snipe-it/commits?author=doekman "Code") | [<img src="https://avatars1.githubusercontent.com/u/4325936?v=4" width="110px;"/><br /><sub>Djamon Staal</sub>](https://www.sdhd.nl/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=SjamonDaal "Code") | [<img src="https://avatars3.githubusercontent.com/u/12306859?v=4" width="110px;"/><br /><sub>Earl Ramirez</sub>](https://github.com/EarlRamirez)<br />[💻](https://github.com/snipe/snipe-it/commits?author=EarlRamirez "Code") | [<img src="https://avatars2.githubusercontent.com/u/8671456?v=4" width="110px;"/><br /><sub>Richard Ray Thomas</sub>](https://github.com/RichardRay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=RichardRay "Code") | [<img src="https://avatars3.githubusercontent.com/u/1852688?v=4" width="110px;"/><br /><sub>Ryan Kuba</sub>](https://www.taisun.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=thelamer "Code") | [<img src="https://avatars1.githubusercontent.com/u/6751928?v=4" width="110px;"/><br /><sub>Brian Monroe</sub>](https://github.com/ParadoxGuitarist)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ParadoxGuitarist "Code") | [<img src="https://avatars1.githubusercontent.com/u/605167?v=4" width="110px;"/><br /><sub>plexorama</sub>](https://github.com/plexorama)<br />[💻](https://github.com/snipe/snipe-it/commits?author=plexorama "Code") |
| [<img src="https://avatars2.githubusercontent.com/u/1795149?v=4" width="110px;"/><br /><sub>Till Deeke</sub>](https://tilldeeke.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tilldeeke "Code") | [<img src="https://avatars0.githubusercontent.com/u/12634129?v=4" width="110px;"/><br /><sub>5quirrel</sub>](https://github.com/5quirrel)<br />[💻](https://github.com/snipe/snipe-it/commits?author=5quirrel "Code") | [<img src="https://avatars1.githubusercontent.com/u/13071957?v=4" width="110px;"/><br /><sub>Jason</sub>](https://github.com/jasonlshelton)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jasonlshelton "Code") | [<img src="https://avatars3.githubusercontent.com/u/7128321?v=4" width="110px;"/><br /><sub>Antti</sub>](https://github.com/chemfy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chemfy "Code") | [<img src="https://avatars3.githubusercontent.com/u/10080364?v=4" width="110px;"/><br /><sub>DeusMaximus</sub>](https://github.com/DeusMaximus)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DeusMaximus "Code") | [<img src="https://avatars2.githubusercontent.com/u/16384611?v=4" width="110px;"/><br /><sub>a-royal</sub>](https://github.com/A-ROYAL)<br />[🌍](#translation-A-ROYAL "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5358208?v=4" width="110px;"/><br /><sub>Alberto Aldrigo</sub>](https://github.com/albertoaldrigo)<br />[🌍](#translation-albertoaldrigo "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/1412342?v=4" width="110px;"/><br /><sub>Alex Stanev</sub>](http://alex.stanev.org/blog)<br />[🌍](#translation-RealEnder "Translation") | [<img src="https://avatars0.githubusercontent.com/u/177295?v=4" width="110px;"/><br /><sub>Andreas Rehm</sub>](http://devel.itsolution2.de)<br />[🌍](#translation-sirrus "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5080535?v=4" width="110px;"/><br /><sub>Andreas Erhard</sub>](https://github.com/xelan)<br />[🌍](#translation-xelan "Translation") | [<img src="https://avatars2.githubusercontent.com/u/142350?v=4" width="110px;"/><br /><sub>Andrés Vanegas Jiménez</sub>](https://github.com/angeldeejay)<br />[🌍](#translation-angeldeejay "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3910403?v=4" width="110px;"/><br /><sub>Antonio Schiavon</sub>](https://github.com/aschiavon91)<br />[🌍](#translation-aschiavon91 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10464547?v=4" width="110px;"/><br /><sub>benunter</sub>](https://github.com/benunter)<br />[🌍](#translation-benunter "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5038647?v=4" width="110px;"/><br /><sub>Borys Żmuda</sub>](http://catweb24.pl)<br />[🌍](#translation-rudashi "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/5539359?v=4" width="110px;"/><br /><sub>chibacityblues</sub>](https://github.com/chibacityblues)<br />[🌍](#translation-chibacityblues "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1954830?v=4" width="110px;"/><br /><sub>Chien Wei Lin</sub>](https://github.com/cwlin0416)<br />[🌍](#translation-cwlin0416 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/11700533?v=4" width="110px;"/><br /><sub>Christian Schuster</sub>](https://github.com/Againstreality)<br />[🌍](#translation-Againstreality "Translation") | [<img src="https://avatars1.githubusercontent.com/u/4308704?v=4" width="110px;"/><br /><sub>Christian Stefanus</sub>](http://chriss.webhostid.com)<br />[🌍](#translation-kopi-item "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3009327?v=4" width="110px;"/><br /><sub>wxcafé</sub>](http://wxcafe.net)<br />[🌍](#translation-wxcafe "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35761525?v=4" width="110px;"/><br /><sub>dpyroc</sub>](https://github.com/dpyroc)<br />[🌍](#translation-dpyroc "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2153639?v=4" width="110px;"/><br /><sub>Daniel Friedlmaier</sub>](http://www.friedlmaier.net)<br />[🌍](#translation-da-friedl "Translation") |
| [<img src="https://avatars1.githubusercontent.com/u/2947640?v=4" width="110px;"/><br /><sub>Daniel Heene</sub>](https://github.com/danielheene)<br />[🌍](#translation-danielheene "Translation") | [<img src="https://avatars3.githubusercontent.com/u/319022?v=4" width="110px;"/><br /><sub>danielcb</sub>](https://github.com/danielcb)<br />[🌍](#translation-danielcb "Translation") | [<img src="https://avatars3.githubusercontent.com/u/15846537?v=4" width="110px;"/><br /><sub>Dominik Senti</sub>](https://github.com/dominiksenti)<br />[🌍](#translation-dominiksenti "Translation") | [<img src="https://avatars0.githubusercontent.com/u/25570954?v=4" width="110px;"/><br /><sub>Eric Gautheron</sub>](http://www.konectik.com)<br />[🌍](#translation-EpixFr "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5732623?v=4" width="110px;"/><br /><sub>Erlend Pilø</sub>](https://erlpil.com)<br />[🌍](#translation-Erlpil "Translation") | [<img src="https://avatars0.githubusercontent.com/u/541832?v=4" width="110px;"/><br /><sub>Fabio Rapposelli</sub>](http://fabio.technology)<br />[🌍](#translation-frapposelli "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3605240?v=4" width="110px;"/><br /><sub>Felipe Barros</sub>](https://github.com/fgbs)<br />[🌍](#translation-fgbs "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/257745?v=4" width="110px;"/><br /><sub>Fernando Possebon</sub>](https://github.com/possebon)<br />[🌍](#translation-possebon "Translation") | [<img src="https://avatars3.githubusercontent.com/u/2540832?v=4" width="110px;"/><br /><sub>gdraque</sub>](https://github.com/gdraque)<br />[🌍](#translation-gdraque "Translation") | [<img src="https://avatars0.githubusercontent.com/u/23440381?v=4" width="110px;"/><br /><sub>Georg Wallisch</sub>](https://github.com/georgwallisch)<br />[🌍](#translation-georgwallisch "Translation") | [<img src="https://avatars1.githubusercontent.com/u/9852832?v=4" width="110px;"/><br /><sub>Gerardo Robles</sub>](https://github.com/jgroblesr85)<br />[🌍](#translation-jgroblesr85 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/11082640?v=4" width="110px;"/><br /><sub>Gluek</sub>](https://t.me/Gluek)<br />[🌍](#translation-mrgluek "Translation") | [<img src="https://avatars0.githubusercontent.com/u/6847946?v=4" width="110px;"/><br /><sub>AdnanAbuShahad</sub>](https://github.com/AdnanAbuShahad)<br />[🌍](#translation-AdnanAbuShahad "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3580608?v=4" width="110px;"/><br /><sub>Hafidzi My</sub>](https://hafidzi.my)<br />[🌍](#translation-hafidzi "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/205521?v=4" width="110px;"/><br /><sub>Harim Park</sub>](https://github.com/fofwisdom)<br />[🌍](#translation-fofwisdom "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3333841?v=4" width="110px;"/><br /><sub>Henrik Kentsson</sub>](http://www.kentsson.se)<br />[🌍](#translation-Kentsson "Translation") | [<img src="https://avatars0.githubusercontent.com/u/36551034?v=4" width="110px;"/><br /><sub>Husnul Yaqien</sub>](https://github.com/husnulyaqien)<br />[🌍](#translation-husnulyaqien "Translation") | [<img src="https://avatars1.githubusercontent.com/u/2372747?v=4" width="110px;"/><br /><sub>Ibrahim</sub>](http://abaalkhail.org)<br />[🌍](#translation-abaalkh "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1389334?v=4" width="110px;"/><br /><sub>igolman</sub>](https://github.com/igolman)<br />[🌍](#translation-igolman "Translation") | [<img src="https://avatars1.githubusercontent.com/u/3257070?v=4" width="110px;"/><br /><sub>itangiang</sub>](https://github.com/itangiang)<br />[🌍](#translation-itangiang "Translation") | [<img src="https://avatars2.githubusercontent.com/u/14814254?v=4" width="110px;"/><br /><sub>jarby1211</sub>](https://github.com/jarby1211)<br />[🌍](#translation-jarby1211 "Translation") |
| [<img src="https://avatars3.githubusercontent.com/u/6719357?v=4" width="110px;"/><br /><sub>Jhonn Willker</sub>](http://jwillker.com)<br />[🌍](#translation-JohnWillker "Translation") | [<img src="https://avatars2.githubusercontent.com/u/10983635?v=4" width="110px;"/><br /><sub>Jose</sub>](https://github.com/joxelito94)<br />[🌍](#translation-joxelito94 "Translation") | [<img src="https://avatars0.githubusercontent.com/u/5206122?v=4" width="110px;"/><br /><sub>laopangzi</sub>](https://github.com/laopangzi)<br />[🌍](#translation-laopangzi "Translation") | [<img src="https://avatars2.githubusercontent.com/u/79707?v=4" width="110px;"/><br /><sub>Lars Strojny</sub>](http://usrportage.de)<br />[🌍](#translation-lstrojny "Translation") | [<img src="https://avatars0.githubusercontent.com/u/389801?v=4" width="110px;"/><br /><sub>MarcosBL</sub>](http://twitter.com/marcosbl)<br />[🌍](#translation-MarcosBL "Translation") | [<img src="https://avatars3.githubusercontent.com/u/35664606?v=4" width="110px;"/><br /><sub>marie joy cajes</sub>](https://github.com/mariejoyacajes)<br />[🌍](#translation-mariejoyacajes "Translation") | [<img src="https://avatars2.githubusercontent.com/u/3052816?v=4" width="110px;"/><br /><sub>Mark S. Johansen</sub>](http://www.markjohansen.dk)<br />[🌍](#translation-msjohansen "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/982885?v=4" width="110px;"/><br /><sub>Martin Stub</sub>](http://martinstub.dk)<br />[🌍](#translation-stubben "Translation") | [<img src="https://avatars2.githubusercontent.com/u/28959963?v=4" width="110px;"/><br /><sub>Meyer Flavio</sub>](https://github.com/meyerf99)<br />[🌍](#translation-meyerf99 "Translation") | [<img src="https://avatars3.githubusercontent.com/u/796443?v=4" width="110px;"/><br /><sub>Micael Rodrigues</sub>](https://github.com/MicaelRodrigues)<br />[🌍](#translation-MicaelRodrigues "Translation") | [<img src="https://avatars0.githubusercontent.com/u/10481331?v=4" width="110px;"/><br /><sub>Mikael Rasmussen</sub>](http://rubixy.com/)<br />[🌍](#translation-mikaelssen "Translation") | [<img src="https://avatars1.githubusercontent.com/u/1544552?v=4" width="110px;"/><br /><sub>IxFail</sub>](https://github.com/IxFail)<br />[🌍](#translation-IxFail "Translation") | [<img src="https://avatars3.githubusercontent.com/u/18483118?v=4" width="110px;"/><br /><sub>Mohammed Fota</sub>](http://www.mohammedfota.com)<br />[🌍](#translation-MohammedFota "Translation") | [<img src="https://avatars0.githubusercontent.com/u/227080?v=4" width="110px;"/><br /><sub>Moayad Alserihi</sub>](https://github.com/omego)<br />[🌍](#translation-omego "Translation") |
| [<img src="https://avatars0.githubusercontent.com/u/1680266?v=4" width="110px;"/><br /><sub>saymd</sub>](https://github.com/saymd)<br />[🌍](#translation-saymd "Translation") | [<img src="https://avatars0.githubusercontent.com/u/1826808?v=4" width="110px;"/><br /><sub>Patrik Larsson</sub>](https://nordsken.se)<br />[🌍](#translation-pooot "Translation") | [<img src="https://avatars1.githubusercontent.com/u/20584746?v=4" width="110px;"/><br /><sub>drcryo</sub>](https://github.com/drcryo)<br />[🌍](#translation-drcryo "Translation") | [<img src="https://avatars1.githubusercontent.com/u/19408004?v=4" width="110px;"/><br /><sub>pawel1615</sub>](https://github.com/pawel1615)<br />[🌍](#translation-pawel1615 "Translation") | [<img src="https://avatars2.githubusercontent.com/u/23340468?v=4" width="110px;"/><br /><sub>bodrovics</sub>](https://github.com/bodrovics)<br />[🌍](#translation-bodrovics "Translation") | [<img src="https://avatars0.githubusercontent.com/u/3257654?v=4" width="110px;"/><br /><sub>priatna</sub>](https://github.com/priatna)<br />[🌍](#translation-priatna "Translation") | [<img src="https://avatars1.githubusercontent.com/u/5358374?v=4" width="110px;"/><br /><sub>Fan Jiang</sub>](https://amayume.net)<br />[🌍](#translation-ProfFan "Translation") |
| [<img src="https://avatars1.githubusercontent.com/u/22555451?v=4" width="110px;"/><br /><sub>ragnarcx</sub>](https://github.com/ragnarcx)<br />[🌍](#translation-ragnarcx "Translation") | [<img src="https://avatars2.githubusercontent.com/u/18654582?v=4" width="110px;"/><br /><sub>Rein van Haaren</sub>](http://www.reinvanhaaren.nl/)<br />[🌍](#translation-reinvanhaaren "Translation") | [<img src="https://avatars1.githubusercontent.com/u/386672?v=4" width="110px;"/><br /><sub>Teguh Dwicaksana</sub>](http://dheche.songolimo.net)<br />[🌍](#translation-dheche "Translation") | [<img src="https://avatars2.githubusercontent.com/u/2572552?v=4" width="110px;"/><br /><sub>fraccie</sub>](https://github.com/FRaccie)<br />[🌍](#translation-FRaccie "Translation") | [<img src="https://avatars0.githubusercontent.com/u/35182720?v=4" width="110px;"/><br /><sub>vinzruzell</sub>](https://github.com/vinzruzell)<br />[🌍](#translation-vinzruzell "Translation") | [<img src="https://avatars1.githubusercontent.com/u/7883603?v=4" width="110px;"/><br /><sub>Kevin Austin</sub>](http://kevinaustin.com)<br />[🌍](#translation-vipsystem "Translation") | [<img src="https://avatars3.githubusercontent.com/u/3861828?v=4" width="110px;"/><br /><sub>Wira Sandy</sub>](http://azuraweb.xyz)<br />[🌍](#translation-wira-sandy "Translation") |
| [<img src="https://avatars2.githubusercontent.com/u/8663789?v=4" width="110px;"/><br /><sub>Илья</sub>](https://github.com/GrayHoax)<br />[🌍](#translation-GrayHoax "Translation") | [<img src="https://avatars3.githubusercontent.com/u/30119111?v=4" width="110px;"/><br /><sub>GodUseVPN</sub>](https://github.com/godusevpn)<br />[🌍](#translation-godusevpn "Translation") | [<img src="https://avatars1.githubusercontent.com/u/745576?v=4" width="110px;"/><br /><sub>周周</sub>](https://github.com/EngrZhou)<br />[🌍](#translation-EngrZhou "Translation") | [<img src="https://avatars3.githubusercontent.com/u/1631095?v=4" width="110px;"/><br /><sub>Sam</sub>](https://github.com/takuy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=takuy "Code") | [<img src="https://avatars1.githubusercontent.com/u/264022?v=4" width="110px;"/><br /><sub>Azerothian</sub>](https://www.illisian.com.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azerothian "Code") | [<img src="https://avatars1.githubusercontent.com/u/7632599?v=4" width="110px;"/><br /><sub>Tim Farmer</sub>](https://github.com/timothyfarmer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=timothyfarmer "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use League\Csv\Reader;
use App\Models\Location;
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->setOffset(1); //because we don't want to insert the header
$results = $csv->fetchAssoc();
// Import parent location names first if they don't exist
foreach ($results as $parent_index => $parent_row) {
$parent_name = trim($parent_row['Parent Name']);
// First create any parents if they don't exist
if ($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(array('name' => $parent_name));
$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 location for '.$parent_row['Name'].' provided.');
}
}
// Loop through ALL records and add/update them if there are additional fields
// besides name
foreach ($results as $index => $row) {
// Set the location attributes to save
$location = Location::firstOrNew(array('name' => trim($row['Name'])));
$location->name = trim($row['Name']);
$location->currency = trim($row['Currency']);
$location->address = trim($row['Address 1']);
$location->address2 = trim($row['Address 2']);
$location->city = trim($row['City']);
$location->state = trim($row['State']);
$location->zip = trim($row['Zip']);
$location->country = trim($row['Country']);
$location->ldap_ou = trim($row['OU']);
$this->info('Checking location: '.$location->name);
// If a parent name nis provided, we created it earlier in the script,
// so let's grab that ID
if ($parent_name) {
$parent = Location::where('name', '=', $parent_name)->first();
$location->parent_id = $parent->id;
$this->info('Parent 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() );
}
}
}
}

View File

@@ -61,16 +61,16 @@ class LdapSync extends Command
$json_summary = [ "error" => true, "error_message" => $e->getMessage(), "summary" => [] ];
$this->info(json_encode($json_summary));
}
LOG::error($e);
LOG::info($e);
return [];
}
$summary = array();
try {
try {
if ($this->option('base_dn') != '') {
$search_base = $this->option('base_dn');
LOG::debug('Importing users from specified base DN: \"'.$search_base.'\".');
LOG::debug('Importing users from specified base DN: \"'.$search_base.'\".');
} else {
$search_base = null;
}
@@ -80,7 +80,7 @@ class LdapSync extends Command
$json_summary = [ "error" => true, "error_message" => $e->getMessage(), "summary" => [] ];
$this->info(json_encode($json_summary));
}
LOG::error($e);
LOG::info($e);
return [];
}
@@ -106,7 +106,7 @@ class LdapSync extends Command
// 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 = array();
foreach ($ldap_ou_locations as $location) {
$ldap_ou_lengths[] = strlen($location["ldap_ou"]);
}
@@ -128,15 +128,21 @@ class LdapSync extends Command
$location_users = Ldap::findLdapUsers($ldap_loc["ldap_ou"]);
$usernames = array();
for ($i = 0; $i < $location_users["count"]; $i++) {
$location_users[$i]["ldap_location_override"] = true;
$location_users[$i]["location_id"] = $ldap_loc["id"];
$usernames[] = $location_users[$i][$ldap_result_username][0];
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 (in_array($generic_entry[$ldap_result_username][0], $usernames)) {
unset($results[$key]);
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]);
}
}
}
@@ -162,30 +168,31 @@ class LdapSync extends Command
$item["ldap_location_override"] = isset($results[$i]["ldap_location_override"]) ? $results[$i]["ldap_location_override"]:"";
$item["location_id"] = isset($results[$i]["location_id"]) ? $results[$i]["location_id"]:"";
if ( array_key_exists('useraccountcontrol', $results[$i]) ) {
$enabled_accounts = [
'512', '544', '66048', '66080', '262656', '262688', '328192', '328224'
];
$item['activated'] = ( in_array($results[$i]['useraccountcontrol'][0], $enabled_accounts) ) ? 1 : 0;
$user = User::where('username', $item["username"])->first();
if ($user) {
// Updating an existing user.
$item["createorupdate"] = 'updated';
} else {
$item['activated'] = 0;
}
// User exists
$item["createorupdate"] = 'updated';
if (!$user = User::where('username', $item["username"])->first()) {
// Creating a new user.
$user = new User;
$user->password = $pass;
$user->activated = 0;
$item["createorupdate"] = 'created';
}
// Create the user if they don't exist.
$user->first_name = e($item["firstname"]);
$user->last_name = e($item["lastname"]);
$user->username = e($item["username"]);
$user->email = e($item["email"]);
$user->employee_num = e($item["employee_number"]);
$user->activated = $item['activated'];
// Sync activated state for Active Directory.
if ( array_key_exists('useraccountcontrol', $results[$i]) ) {
$enabled_accounts = [
'512', '544', '66048', '66080', '262656', '262688', '328192', '328224'
];
$user->activated = ( in_array($results[$i]['useraccountcontrol'][0], $enabled_accounts) ) ? 1 : 0;
}
if ($item['ldap_location_override'] == true) {
$user->location_id = $item['location_id'];
@@ -199,7 +206,6 @@ class LdapSync extends Command
}
$user->notes = 'Imported from LDAP';
$user->ldap_import = 1;
$errors = '';

View File

@@ -74,6 +74,7 @@ class ObjectImportCommand extends Command
$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'));
$logFile = $this->option('logfile');
@@ -172,6 +173,7 @@ class ObjectImportCommand extends Command
array('web-importer', null, InputOption::VALUE_NONE, 'Internal: packages output for use with the web importer'),
array('user_id', null, InputOption::VALUE_REQUIRED, 'ID of user creating items', 1),
array('update', null, InputOption::VALUE_NONE, 'If a matching item is found, update item information'),
array('send-welcome', null, InputOption::VALUE_NONE, 'Whether to send a welcome email to any new users that are created.'),
);
}

View File

@@ -79,6 +79,7 @@ class PaveIt extends Command
DB::statement('delete from accessories_users');
DB::statement('delete from asset_logs');
DB::statement('delete from asset_maintenances');
DB::statement('delete from login_attempts');
DB::statement('delete from asset_uploads');
DB::statement('delete from action_logs');
DB::statement('delete from checkout_requests');

View File

@@ -63,6 +63,8 @@ class ResetDemoSettings extends Command
$settings->time_display_format = 'g:iA';
$settings->thumbnail_max_h = '30';
$settings->locale = 'en';
$settings->version_footer = 'on';
$settings->support_footer = 'on';
$settings->save();
if ($user = User::where('username', '=', 'admin')->first()) {

View File

@@ -0,0 +1,120 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Consumable;
use App\Models\Accessory;
use App\Models\LicenseSeat;
use App\Models\License;
use DB;
use Artisan;
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');
}
}

View File

@@ -57,12 +57,12 @@ class SendExpectedCheckinAlerts extends Command
}
}
// Send a rollup to the admin, if settings dictate
$recipient = new \App\Models\Recipients\AlertRecipient();
if (($assets) && ($assets->count() > 0) && ($settings->alert_email!='')) {
$recipient->notify(new ExpectedCheckinAdminNotification($assets));
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, $key) {
return new \App\Models\Recipients\AlertRecipient($item);
});
\Notification::send($recipients, new ExpectedCheckinAdminNotification($assets));
}

View File

@@ -6,6 +6,8 @@ use App\Models\Asset;
use App\Models\License;
use App\Models\Setting;
use DB;
use App\Notifications\ExpiringLicenseNotification;
use App\Notifications\ExpiringAssetsNotification;
use Illuminate\Console\Command;
@@ -44,88 +46,41 @@ class SendExpirationAlerts extends Command
public function fire()
{
// Expiring Assets
$expiring_assets = Asset::getExpiringWarrantee(Setting::getSettings()->alert_interval);
$this->info(count($expiring_assets).' expiring assets');
$asset_data['count'] = count($expiring_assets);
$asset_data['email_content'] ='';
$now = date("Y-m-d");
$settings = Setting::getSettings();
$threshold = $settings->alert_interval;
foreach ($expiring_assets as $asset) {
if (($settings->alert_email != '') && ($settings->alerts_enabled == 1)) {
$expires = $asset->present()->warrantee_expires();
$difference = round(abs(strtotime($expires) - strtotime($now))/86400);
if ($difference > 30) {
$asset_data['email_content'] .= '<tr style="background-color: #fcffa3;">';
} else {
$asset_data['email_content'] .= '<tr style="background-color:#d9534f;">';
}
$asset_data['email_content'] .= '<td><a href="'.config('app.url').'/hardware/'.e($asset->id).'/view">';
$asset_data['email_content'] .= $asset->present()->name().'</a></td><td>'.e($asset->asset_tag).'</td>';
$asset_data['email_content'] .= '<td>'.e($asset->present()->warrantee_expires()).'</td>';
$asset_data['email_content'] .= '<td>'.$difference.' '.trans('mail.days').'</td>';
$asset_data['email_content'] .= '<td>'.($asset->supplier ? e($asset->supplier->name) : '').'</td>';
$asset_data['email_content'] .= '<td>'.($asset->assignedTo ? e($asset->assignedTo->present()->name()) : '').'</td>';
$asset_data['email_content'] .= '</tr>';
}
// Expiring licenses
$expiring_licenses = License::getExpiringLicenses(Setting::getSettings()->alert_interval);
$this->info(count($expiring_licenses).' expiring licenses');
$license_data['count'] = $expiring_licenses->count();
$license_data['email_content'] = '';
foreach ($expiring_licenses as $license) {
$expires = $license->expiration_date;
$difference = round(abs(strtotime($expires) - strtotime($now))/86400);
if ($difference > 30) {
$license_data['email_content'] .= '<tr style="background-color: #fcffa3;">';
} else {
$license_data['email_content'] .= '<tr style="background-color:#d9534f;">';
}
$license_data['email_content'] .= '<td><a href="'.route('licenses.show', $license->id).'">';
$license_data['email_content'] .= $license->name.'</a></td>';
$license_data['email_content'] .= '<td>'.$license->expiration_date.'</td>';
$license_data['email_content'] .= '<td>'.$difference.' days</td>';
$license_data['email_content'] .= '</tr>';
}
if ((Setting::getSettings()->alert_email!='') && (Setting::getSettings()->alerts_enabled==1)) {
if (count($expiring_assets) > 0) {
$this->info('Report sent to '.Setting::getSettings()->alert_email);
\Mail::send('emails.expiring-assets-report', $asset_data, function ($m) {
$m->to(explode(',', Setting::getSettings()->alert_email), Setting::getSettings()->site_name);
$m->replyTo(config('mail.reply_to.address'), config('mail.reply_to.name'));
$m->subject(trans('mail.Expiring_Assets_Report'));
});
// Send a rollup to the admin, if settings dictate
$recipients = collect(explode(',', $settings->alert_email))->map(function ($item, $key) {
return new \App\Models\Recipients\AlertRecipient($item);
});
// Expiring Assets
$assets = Asset::getExpiringWarrantee(Setting::getSettings()->alert_interval);
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));
}
if (count($expiring_licenses) > 0) {
$this->info('Report sent to '.Setting::getSettings()->alert_email);
\Mail::send('emails.expiring-licenses-report', $license_data, function ($m) {
$m->to(explode(',', Setting::getSettings()->alert_email), Setting::getSettings()->site_name);
$m->replyTo(config('mail.reply_to.address'), config('mail.reply_to.name'));
$m->subject(trans('mail.Expiring_Licenses_Report'));
});
// 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 (Setting::getSettings()->alert_email=='') {
echo "Could not send email. No alert email configured in settings. \n";
} elseif (Setting::getSettings()->alerts_enabled!=1) {
echo "Alerts are disabled in the settings. No mail will be sent. \n";
if ($settings->alert_email=='') {
$this->error('Could not send email. No alert email configured in settings');
} elseif ($settings->alerts_enabled!=1) {
$this->info('Alerts are disabled in the settings. No mail will be sent');
}
}

View File

@@ -6,6 +6,7 @@ use App\Models\Setting;
use DB;
use Mail;
use App\Helpers\Helper;
use App\Notifications\InventoryAlert;
use Illuminate\Console\Command;
@@ -42,25 +43,28 @@ class SendInventoryAlerts extends Command
*/
public function handle()
{
if ((Setting::getSettings()->alert_email!='') && (Setting::getSettings()->alerts_enabled==1)) {
$settings = Setting::getSettings();
$data['data'] = Helper::checkLowInventory();
$data['count'] = count($data['data']);
if (($settings->alert_email!='') && ($settings->alerts_enabled==1)) {
if (count($data['data']) > 0) {
\Mail::send('emails.low-inventory', $data, function ($m) {
$m->to(explode(',', Setting::getSettings()->alert_email), Setting::getSettings()->site_name);
$m->replyTo(config('mail.reply_to.address'), config('mail.reply_to.name'));
$m->subject(trans('mail.Low_Inventory_Report'));
$items = Helper::checkLowInventory();
// Send a rollup to the admin, if settings dictate
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 \App\Models\Recipients\AlertRecipient($item);
});
\Notification::send($recipients, new InventoryAlert($items, $settings->alert_threshold));
}
} else {
if (Setting::getSettings()->alert_email=='') {
echo "Could not send email. No alert email configured in settings. \n";
$this->error('Could not send email. No alert email configured in settings');
} elseif (Setting::getSettings()->alerts_enabled!=1) {
echo "Alerts are disabled in the settings. No mail will be sent. \n";
$this->info('Alerts are disabled in the settings. No mail will be sent');
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\License;
use App\Models\Setting;
use App\Notifications\ExpiringAssetsNotification;
use App\Models\Recipients;
use DB;
use Illuminate\Console\Command;
use App\Notifications\SendUpcomingAuditNotification;
use Carbon\Carbon;
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();
if (($settings->alert_email != '') && ($settings->audit_warning_days) && ($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 \App\Models\Recipients\AlertRecipient($item);
});
// Assets due for auditing
$assets = Asset::whereNotNull('next_audit_date')
->DueOrOverdueForAudit($settings)
->orderBy('last_audit_date', 'asc')->get();
if ($assets->count() > 0) {
$this->info(trans_choice('mail.upcoming-audits', $assets->count(),
['count' => $assets->count(), 'threshold' => $settings->audit_warning_days]));
\Notification::send($recipients, new SendUpcomingAuditNotification($assets, $settings->audit_warning_days));
$this->info('Audit report sent to '.$settings->alert_email);
} else {
$this->info('No assets to be audited. No report sent.');
}
} elseif ($settings->alert_email=='') {
$this->error('Could not send email. No alert email configured in settings');
} elseif (!$settings->audit_warning_days) {
$this->error('No audit warning days set in Admin Notifications. No mail will be sent.');
} elseif ($settings->alerts_enabled!=1) {
$this->info('Alerts are disabled in the settings. No mail will be sent');
} else {
$this->error('Something went wrong. :( ');
$this->error('Admin Notifications Email Setting: '.$settings->alert_email);
$this->error('Admin Audit Warning Setting: '.$settings->audit_warning_days);
$this->error('Admin Alerts Emnabled: '.$settings->alerts_enabled);
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Console;
use App\Console\Commands\RestoreDeletedUsers;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -30,6 +31,9 @@ class Kernel extends ConsoleKernel
Commands\SyncAssetLocations::class,
Commands\RegenerateAssetTags::class,
Commands\SyncAssetCounters::class,
Commands\RestoreDeletedUsers::class,
Commands\SendUpcomingAuditReport::class,
Commands\ImportLocations::class,
];
/**
@@ -46,6 +50,7 @@ class Kernel extends ConsoleKernel
$schedule->command('snipeit:expected-checkin')->daily();
$schedule->command('snipeit:backup')->weekly();
$schedule->command('backup:clean')->daily();
$schedule->command('snipeit:upcoming-audits')->daily();
}
protected function commands()

View File

@@ -5,8 +5,17 @@ namespace App\Exceptions;
use Exception;
class CheckoutNotAllowed extends Exception
{
private $errorMessage;
function __construct($errorMessage = null)
{
$this->errorMessage = $errorMessage;
parent::__construct($errorMessage);
}
public function __toString()
{
return "A checkout is not allowed under these circumstances";
return is_null($this->errorMessage) ? "A checkout is not allowed under these circumstances" : $this->errorMessage;
}
}

View File

@@ -137,95 +137,6 @@ class Helper
return floatval($floatString);
}
/**
* Get the list of models in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return Array
*/
public static function modelList()
{
$models = AssetModel::with('manufacturer')->get();
$model_array[''] = trans('general.select_model');
foreach ($models as $model) {
$model_array[$model->id] = $model->present()->modelName();
}
return $model_array;
}
/**
* Get the list of companies in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return Array
*/
public static function companyList()
{
$company_list = array('' => trans('general.select_company')) + DB::table('companies')
->orderBy('name', 'asc')
->pluck('name', 'id')
->toArray();
return $company_list;
}
/**
* Get the list of categories in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return Array
*/
public static function categoryList($category_type = null)
{
$categories = Category::orderBy('name', 'asc')
->whereNull('deleted_at')
->orderBy('name', 'asc');
if (!empty($category_type)) {
$categories = $categories->where('category_type', '=', $category_type);
}
$category_list = array('' => trans('general.select_category')) + $categories->pluck('name', 'id')->toArray();
return $category_list;
}
/**
* Get the list of categories in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return Array
*/
public static function departmentList()
{
$departments = Department::orderBy('name', 'asc')
->whereNull('deleted_at')
->orderBy('name', 'asc');
return array('' => trans('general.select_department')) + $departments->pluck('name', 'id')->toArray();
}
/**
* Get the list of suppliers in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return Array
*/
public static function suppliersList()
{
$supplier_list = array('' => trans('general.select_supplier')) + Supplier::orderBy('name', 'asc')
->orderBy('name', 'asc')
->pluck('name', 'id')->toArray();
return $supplier_list;
}
/**
* Get the list of status labels in an array to make a dropdown menu
*
@@ -240,37 +151,6 @@ class Helper
return $statuslabel_list;
}
/**
* Get the list of locations in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return Array
*/
public static function locationsList()
{
$location_list = array('' => trans('general.select_location')) + Location::orderBy('name', 'asc')
->pluck('name', 'id')->toArray();
return $location_list;
}
/**
* Get the list of manufacturers in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return Array
*/
public static function manufacturerList()
{
$manufacturer_list = array('' => trans('general.select_manufacturer')) +
Manufacturer::orderBy('name', 'asc')
->pluck('name', 'id')->toArray();
return $manufacturer_list;
}
/**
* Get the list of status label types in an array to make a dropdown menu
*
@@ -289,24 +169,6 @@ class Helper
return $statuslabel_types;
}
/**
* Get the list of managers in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return Array
*/
public static function managerList()
{
$manager_list = array('' => trans('general.select_user')) +
User::where('deleted_at', '=', null)
->orderBy('last_name', 'asc')
->orderBy('first_name', 'asc')->get()
->pluck('complete_name', 'id')->toArray();
return $manager_list;
}
/**
* Get the list of depreciations in an array to make a dropdown menu
*
@@ -341,54 +203,6 @@ class Helper
return $category_types;
}
/**
* Get the list of users in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return Array
*/
public static function usersList()
{
$users_list = array( '' => trans('general.select_user')) +
Company::scopeCompanyables(User::where('deleted_at', '=', null))
->where('show_in_list', '=', 1)
->orderBy('last_name', 'asc')
->orderBy('first_name', 'asc')->get()
->pluck('complete_name', 'id')->toArray();
return $users_list;
}
/**
* Get the list of assets in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return Array
*/
public static function assetsList()
{
$assets_list = array('' => trans('general.select_asset')) + Asset::orderBy('name', 'asc')
->whereNull('deleted_at')
->pluck('name', 'id')->toArray();
return $assets_list;
}
/**
* Get the detailed list of assets in an array to make a dropdown menu
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.5]
* @return array
*/
public static function detailedAssetList()
{
$assets = array('' => trans('general.select_asset')) + Company::scopeCompanyables(Asset::with('assignedTo', 'model'), 'assets.company_id')->get()->pluck('detailed_name', 'id')->toArray();
return $assets;
}
/**
* Get the list of custom fields in an array to make a dropdown menu
*
@@ -476,7 +290,7 @@ class Helper
$all_count = 0;
foreach ($consumables as $consumable) {
$avail = $consumable->qty - $consumable->consumable_assignment_count; //$consumable->numRemaining();
$avail = $consumable->numRemaining();
if ($avail < ($consumable->min_amt) + \App\Models\Setting::getSettings()->alert_threshold) {
if ($consumable->qty > 0) {
$percent = number_format((($avail / $consumable->qty) * 100), 0);

View File

@@ -166,10 +166,8 @@ class AccessoriesController extends Controller
$accessory->supplier_id = request('supplier_id');
if ($request->hasFile('image')) {
if (!config('app.lock_passwords')) {
$image = $request->file('image');
$ext = $image->getClientOriginalExtension();
$file_name = "accessory-".str_random(18).'.'.$ext;

View File

@@ -24,7 +24,7 @@ class AccessoriesController extends Controller
$this->authorize('view', Accessory::class);
$allowed_columns = ['id','name','model_number','eol','notes','created_at','min_amt','company_id'];
$accessories = Accessory::whereNull('accessories.deleted_at')->with('category', 'company', 'manufacturer', 'users', 'location');
$accessories = Accessory::with('category', 'company', 'manufacturer', 'users', 'location');
if ($request->has('search')) {
$accessories = $accessories->TextSearch($request->input('search'));
@@ -46,7 +46,7 @@ class AccessoriesController extends Controller
$accessories->where('supplier_id','=',$request->input('supplier_id'));
}
$offset = $request->input('offset', 0);
$offset = (($accessories) && (request('offset') > $accessories->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';

View File

@@ -44,7 +44,7 @@ class AssetMaintenancesController extends Controller
$maintenances->where('asset_id', '=', $request->input('asset_id'));
}
$offset = request('offset', 0);
$offset = (($maintenances) && (request('offset') > $maintenances->count())) ? 0 : request('offset', 0);
$limit = request('limit', 50);
$allowed_columns = [

View File

@@ -60,7 +60,7 @@ class AssetModelsController extends Controller
$assetmodels->TextSearch($request->input('search'));
}
$offset = $request->input('offset', 0);
$offset = (($assetmodels) && (request('offset') > $assetmodels->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'models.created_at';
@@ -145,7 +145,7 @@ class AssetModelsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', AssetModel::class);
$this->authorize('update', AssetModel::class);
$assetmodel = AssetModel::findOrFail($id);
$assetmodel->fill($request->all());
$assetmodel->fieldset_id = $request->get("custom_fieldset_id");
@@ -179,7 +179,7 @@ class AssetModelsController extends Controller
try {
unlink(public_path().'/uploads/models/'.$assetmodel->image);
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}

View File

@@ -52,7 +52,7 @@ class AssetsController extends Controller
* @since [v4.0]
* @return JsonResponse
*/
public function index(Request $request)
public function index(Request $request, $audit = null)
{
$this->authorize('index', Asset::class);
@@ -95,7 +95,7 @@ class AssetsController extends Controller
$assets = Company::scopeCompanyables(Asset::select('assets.*'),"company_id","assets")
->with('location', 'assetstatus', 'assetlog', 'company', 'defaultLoc','assignedTo',
'model.category', 'model.manufacturer', 'model.fieldset','supplier');
'model.category', 'model.manufacturer', 'model.fieldset','supplier');
// These are used by the API to query against specific ID numbers.
@@ -127,7 +127,7 @@ class AssetsController extends Controller
if (($request->has('assigned_to')) && ($request->has('assigned_type'))) {
$assets->where('assets.assigned_to', '=', $request->input('assigned_to'))
->where('assets.assigned_type', '=', $request->input('assigned_type'));
->where('assets.assigned_type', '=', $request->input('assigned_type'));
}
if ($request->has('company_id')) {
@@ -144,10 +144,25 @@ class AssetsController extends Controller
$request->has('order_number') ? $assets = $assets->where('assets.order_number', '=', e($request->get('order_number'))) : '';
$offset = request('offset', 0);
$offset = (($assets) && (request('offset') > $assets->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
// This is used by the audit reporting routes
if (Gate::allows('audit', Asset::class)) {
switch ($audit) {
case 'due':
$assets->DueOrOverdueForAudit($settings);
break;
case 'overdue':
$assets->overdueForAudit($settings);
break;
}
}
// This is used by the sidenav, mostly
// We switched from using query scopes here because of a Laravel bug
@@ -167,12 +182,12 @@ class AssetsController extends Controller
break;
case 'RTD':
$assets->whereNull('assets.assigned_to')
->join('status_labels AS status_alias',function ($join) {
$join->on('status_alias.id', "=", "assets.status_id")
->where('status_alias.deployable','=',1)
->where('status_alias.pending','=',0)
->where('status_alias.archived', '=', 0);
});
->join('status_labels AS status_alias',function ($join) {
$join->on('status_alias.id', "=", "assets.status_id")
->where('status_alias.deployable','=',1)
->where('status_alias.pending','=',0)
->where('status_alias.archived', '=', 0);
});
break;
case 'Undeployable':
$assets->Undeployable();
@@ -188,11 +203,11 @@ class AssetsController extends Controller
case 'Requestable':
$assets->where('assets.requestable', '=', 1)
->join('status_labels AS status_alias',function ($join) {
$join->on('status_alias.id', "=", "assets.status_id")
->where('status_alias.deployable','=',1)
->where('status_alias.pending','=',0)
->where('status_alias.archived', '=', 0);
});
$join->on('status_alias.id', "=", "assets.status_id")
->where('status_alias.deployable','=',1)
->where('status_alias.pending','=',0)
->where('status_alias.archived', '=', 0);
});
break;
case 'Deployed':
@@ -207,8 +222,8 @@ class AssetsController extends Controller
$join->on('status_alias.id', "=", "assets.status_id")
->where('status_alias.archived', '=', 0);
});
// If there is a status ID, don't take show_archived_in_list into consideration
// If there is a status ID, don't take show_archived_in_list into consideration
} else {
$assets->join('status_labels AS status_alias',function ($join) {
$join->on('status_alias.id', "=", "assets.status_id");
@@ -233,8 +248,8 @@ class AssetsController extends Controller
// This handles all of the pivot sorting (versus the assets.* fields
// in the allowed_columns array)
$column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.created_at';
switch ($sort_override) {
case 'model':
$assets->OrderModels($order);
@@ -291,7 +306,7 @@ class AssetsController extends Controller
$this->authorize('view', $asset);
return (new AssetsTransformer)->transformAsset($asset);
}
return response()->json(Helper::formatStandardApiResponse('error', null, 'Asset not found'), 404);
return response()->json(Helper::formatStandardApiResponse('error', null, 'Asset not found'), 200);
}
@@ -305,17 +320,18 @@ class AssetsController extends Controller
*/
public function showBySerial($serial)
{
$this->authorize('index', Asset::class);
if ($assets = Asset::with('assetstatus')->with('assignedTo')
->withTrashed()->where('serial',$serial)->get()) {
$this->authorize('view', $assets);
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
}
return response()->json(Helper::formatStandardApiResponse('error', null, 'Asset not found'), 404);
return response()->json(Helper::formatStandardApiResponse('error', null, 'Asset not found'), 200);
}
/**
/**
* Returns JSON with information about an asset for detail view.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
@@ -353,7 +369,7 @@ class AssetsController extends Controller
'assets.assigned_to',
'assets.assigned_type',
'assets.status_id'
])->with('model', 'assetstatus', 'assignedTo')->NotArchived());
])->with('model', 'assetstatus', 'assignedTo')->NotArchived(),'company_id', 'assets');
if ($request->has('assetStatusType') && $request->input('assetStatusType') === 'RTD') {
$assets = $assets->RTD();
@@ -378,7 +394,7 @@ class AssetsController extends Controller
$asset->use_text .= ' → '.$asset->assigned->getFullNameAttribute();
}
if ($asset->assetstatus->getStatuslabelType()=='pending') {
$asset->use_text .= '('.$asset->assetstatus->getStatuslabelType().')';
}
@@ -465,46 +481,18 @@ class AssetsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Asset::class);
$this->authorize('update', Asset::class);
if ($asset = Asset::find($id)) {
($request->has('model_id')) ?
$asset->model()->associate(AssetModel::find($request->get('model_id'))) : '';
($request->has('name')) ?
$asset->name = $request->get('name') : '';
($request->has('serial')) ?
$asset->serial = $request->get('serial') : '';
($request->has('model_id')) ?
$asset->model_id = $request->get('model_id') : '';
($request->has('order_number')) ?
$asset->order_number = $request->get('order_number') : '';
($request->has('notes')) ?
$asset->notes = $request->get('notes') : '';
($request->has('asset_tag')) ?
$asset->asset_tag = $request->get('asset_tag') : '';
($request->has('archived')) ?
$asset->archived = $request->get('archived') : '';
($request->has('status_id')) ?
$asset->status_id = $request->get('status_id') : '';
($request->has('warranty_months')) ?
$asset->warranty_months = $request->get('warranty_months') : '';
($request->has('purchase_cost')) ?
$asset->purchase_cost = Helper::ParseFloat($request->get('purchase_cost')) : '';
($request->has('purchase_date')) ?
$asset->purchase_date = $request->get('purchase_date') : '';
($request->has('assigned_to')) ?
$asset->assigned_to = $request->get('assigned_to') : '';
($request->has('supplier_id')) ?
$asset->supplier_id = $request->get('supplier_id') : '';
($request->has('requestable')) ?
$asset->requestable = $request->get('requestable') : '';
($request->has('rtd_location_id')) ?
$asset->rtd_location_id = $request->get('rtd_location_id') : '';
($request->has('rtd_location_id')) ?
$asset->location_id = $request->get('rtd_location_id') : '';
($request->has('company_id')) ?
$asset->company_id = Company::getIdForCurrentUser($request->get('company_id')) : '';
$asset->fill($request->all());
($request->has('model_id')) ?
$asset->model()->associate(AssetModel::find($request->get('model_id'))) : null;
($request->has('company_id')) ?
$asset->company_id = Company::getIdForCurrentUser($request->get('company_id')) : null;
($request->has('rtd_location_id')) ?
$asset->location_id = $request->get('rtd_location_id') : null;
// Update custom fields
if (($model = AssetModel::find($asset->model_id)) && (isset($model->fieldset))) {
@@ -519,9 +507,9 @@ class AssetsController extends Controller
if ($asset->save()) {
if (($request->has('assigned_user')) && ($target = User::find($request->get('assigned_user')))) {
$location = $target->location_id;
$location = $target->location_id;
} elseif (($request->has('assigned_asset')) && ($target = Asset::find($request->get('assigned_asset')))) {
$location = $target->location_id;
$location = $target->location_id;
} elseif (($request->has('assigned_location')) && ($target = Location::find($request->get('assigned_location')))) {
$location = $target->id;
}
@@ -631,7 +619,7 @@ class AssetsController extends Controller
$expected_checkin = request('expected_checkin', null);
$note = request('note', null);
$asset_name = request('name', null);
// Set the location ID to the RTD location id if there is one
if ($asset->rtd_location_id!='') {
$asset->location_id = $target->rtd_location_id;
@@ -639,7 +627,7 @@ class AssetsController extends Controller
if ($asset->checkOut($target, Auth::user(), $checkout_at, $expected_checkin, $note, $asset_name, $asset->location_id)) {
return response()->json(Helper::formatStandardApiResponse('success', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkout.success')));
@@ -755,7 +743,8 @@ class AssetsController extends Controller
*/
public function requestable(Request $request)
{
$this->authorize('viewRequestable', Asset::class);
$assets = Company::scopeCompanyables(Asset::select('assets.*'),"company_id","assets")
->with('location', 'assetstatus', 'assetlog', 'company', 'defaultLoc','assignedTo',
'model.category', 'model.manufacturer', 'model.fieldset','supplier')->where('assets.requestable', '=', '1');

View File

@@ -30,7 +30,7 @@ class CategoriesController extends Controller
$categories = $categories->TextSearch($request->input('search'));
}
$offset = $request->input('offset', 0);
$offset = (($categories) && (request('offset') > $categories->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'assets_count';
@@ -92,7 +92,7 @@ class CategoriesController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Category::class);
$this->authorize('update', Category::class);
$category = Category::findOrFail($id);
$category->fill($request->all());

View File

@@ -41,7 +41,7 @@ class CompaniesController extends Controller
$companies->TextSearch($request->input('search'));
}
$offset = $request->input('offset', 0);
$offset = (($companies) && (request('offset') > $companies->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -104,7 +104,7 @@ class CompaniesController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Company::class);
$this->authorize('update', Company::class);
$company = Company::findOrFail($id);
$company->fill($request->all());

View File

@@ -24,7 +24,7 @@ class ComponentsController extends Controller
public function index(Request $request)
{
$this->authorize('view', Component::class);
$components = Company::scopeCompanyables(Component::select('components.*')->whereNull('components.deleted_at')
$components = Company::scopeCompanyables(Component::select('components.*')
->with('company', 'location', 'category'));
if ($request->has('search')) {
@@ -39,7 +39,11 @@ class ComponentsController extends Controller
$components->where('category_id','=',$request->input('category_id'));
}
$offset = request('offset', 0);
if ($request->has('location_id')) {
$components->where('location_id','=',$request->input('location_id'));
}
$offset = (($components) && (request('offset') > $components->count())) ? 0 : request('offset', 0);
$limit = request('limit', 50);
$allowed_columns = ['id','name','min_amt','order_number','serial','purchase_date','purchase_cost','company','category','qty','location','image'];
@@ -116,7 +120,7 @@ class ComponentsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Component::class);
$this->authorize('update', Component::class);
$component = Component::findOrFail($id);
$component->fill($request->all());

View File

@@ -24,7 +24,6 @@ class ConsumablesController extends Controller
$this->authorize('index', Consumable::class);
$consumables = Company::scopeCompanyables(
Consumable::select('consumables.*')
->whereNull('consumables.deleted_at')
->with('company', 'location', 'category', 'users', 'manufacturer')
);
@@ -36,12 +35,16 @@ class ConsumablesController extends Controller
$consumables->where('company_id','=',$request->input('company_id'));
}
if ($request->has('category_id')) {
$consumables->where('category_id','=',$request->input('category_id'));
}
if ($request->has('manufacturer_id')) {
$consumables->where('manufacturer_id','=',$request->input('manufacturer_id'));
}
$offset = request('offset', 0);
$offset = (($consumables) && (request('offset') > $consumables->count())) ? 0 : request('offset', 0);
$limit = request('limit', 50);
$allowed_columns = ['id','name','order_number','min_amt','purchase_date','purchase_cost','company','category','model_number', 'item_no', 'manufacturer','location','qty','image'];
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
@@ -121,7 +124,7 @@ class ConsumablesController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Consumable::class);
$this->authorize('update', Consumable::class);
$consumable = Consumable::findOrFail($id);
$consumable->fill($request->all());

View File

@@ -57,9 +57,15 @@ class CustomFieldsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', CustomField::class);
$this->authorize('update', CustomField::class);
$field = CustomField::findOrFail($id);
$data = $request->all();
/**
* Updated values for the field,
* without the "field_encrypted" flag, preventing the change of encryption status
* @var array
*/
$data = $request->except(['field_encrypted']);
$validator = Validator::make($data, $field->validationRules());
if ($validator->fails()) {
@@ -106,6 +112,9 @@ class CustomFieldsController extends Controller
public function postReorder(Request $request, $id)
{
$fieldset = CustomFieldset::find($id);
$this->authorize('update', $fieldset);
$fields = array();
$order_array = array();
@@ -125,7 +134,8 @@ class CustomFieldsController extends Controller
public function associate(Request $request, $field_id)
{
$this->authorize('edit', CustomFieldset::class);
$this->authorize('update', CustomFieldset::class);
$field = CustomField::findOrFail($field_id);
$fieldset_id = $request->input('fieldset_id');
@@ -142,7 +152,8 @@ class CustomFieldsController extends Controller
public function disassociate(Request $request, $field_id)
{
$this->authorize('edit', CustomFieldset::class);
$this->authorize('update', CustomFieldset::class);
$field = CustomField::findOrFail($field_id);
$fieldset_id = $request->input('fieldset_id');
@@ -167,6 +178,8 @@ class CustomFieldsController extends Controller
{
$field = CustomField::findOrFail($field_id);
$this->authorize('delete', $field);
if ($field->fieldset->count() >0) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Field is in use.'));
}

View File

@@ -79,7 +79,7 @@ class CustomFieldsetsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', CustomFieldset::class);
$this->authorize('update', CustomFieldset::class);
$fieldset = CustomFieldset::findOrFail($id);
$fieldset->fill($request->all());

View File

@@ -39,7 +39,7 @@ class DepartmentsController extends Controller
$departments = $departments->TextSearch($request->input('search'));
}
$offset = $request->input('offset', 0);
$offset = (($departments) && (request('offset') > $departments->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -114,6 +114,8 @@ class DepartmentsController extends Controller
{
$department = Department::findOrFail($id);
$this->authorize('delete', $department);
if ($department->users->count() > 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/departments/message.assoc_users')));
}

View File

@@ -28,7 +28,7 @@ class DepreciationsController extends Controller
$depreciations = $depreciations->TextSearch($request->input('search'));
}
$offset = $request->input('offset', 0);
$offset = (($depreciations) && (request('offset') > $depreciations->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -88,7 +88,7 @@ class DepreciationsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Depreciation::class);
$this->authorize('update', Depreciation::class);
$depreciation = Depreciation::findOrFail($id);
$depreciation->fill($request->all());

View File

@@ -28,7 +28,7 @@ class GroupsController extends Controller
$groups = $groups->TextSearch($request->input('search'));
}
$offset = $request->input('offset', 0);
$offset = (($groups) && (request('offset') > $groups->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -88,7 +88,7 @@ class GroupsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Group::class);
$this->authorize('update', Group::class);
$group = Group::findOrFail($id);
$group->fill($request->all());

View File

@@ -25,7 +25,7 @@ class ImportController extends Controller
*/
public function index()
{
//
$this->authorize('import');
$imports = Import::latest()->get();
return (new ImportsTransformer)->transformImports($imports);
@@ -39,10 +39,8 @@ class ImportController extends Controller
*/
public function store()
{
//
if (!Company::isCurrentUserAuthorized()) {
return redirect()->route('hardware.index')->with('error', trans('general.insufficient_permissions'));
} elseif (!config('app.lock_passwords')) {
$this->authorize('import');
if (!config('app.lock_passwords')) {
$files = Input::file('files');
$path = config('app.private_uploads').'/imports';
$results = [];
@@ -119,7 +117,7 @@ class ImportController extends Controller
*/
public function process(ItemImportRequest $request, $import_id)
{
$this->authorize('create', Asset::class);
$this->authorize('import');
// Run a backup immediately before processing
Artisan::call('backup:run');
$errors = $request->import(Import::find($import_id));
@@ -162,7 +160,7 @@ class ImportController extends Controller
*/
public function destroy($import_id)
{
$this->authorize('create', Asset::class);
$this->authorize('import');
$import = Import::find($import_id);
try {
unlink(config('app.private_uploads').'/imports/'.$import->file_path);

View File

@@ -64,8 +64,8 @@ class LicensesController extends Controller
$licenses->where('supplier_id','=',$request->input('supplier_id'));
}
if ($request->has('category_i')) {
$licenses->where('category_i','=',$request->input('category_i'));
if ($request->has('category_id')) {
$licenses->where('category_id','=',$request->input('category_id'));
}
if ($request->has('depreciation_id')) {
@@ -82,7 +82,7 @@ class LicensesController extends Controller
}
$offset = request('offset', 0);
$offset = (($licenses) && (request('offset') > $licenses->count())) ? 0 : request('offset', 0);
$limit = request('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
@@ -168,7 +168,7 @@ class LicensesController extends Controller
public function update(Request $request, $id)
{
//
$this->authorize('edit', License::class);
$this->authorize('update', License::class);
$license = License::findOrFail($id);
$license->fill($request->all());
@@ -223,9 +223,12 @@ class LicensesController extends Controller
if ($license = License::find($licenseId)) {
$this->authorize('view', $license);
$seats = LicenseSeat::where('license_id', $licenseId)->with('license', 'user', 'asset');
$offset = request('offset', 0);
$offset = (($seats) && (request('offset') > $seats->count())) ? 0 : request('offset', 0);
$limit = request('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';

View File

@@ -51,7 +51,7 @@ class LocationsController extends Controller
$offset = $request->input('offset', 0);
$offset = (($locations) && (request('offset') > $locations->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -106,7 +106,26 @@ class LocationsController extends Controller
public function show($id)
{
$this->authorize('view', Location::class);
$location = Location::findOrFail($id);
$location = Location::with('parent', 'manager', 'childLocations')
->select([
'locations.id',
'locations.name',
'locations.address',
'locations.address2',
'locations.city',
'locations.state',
'locations.zip',
'locations.country',
'locations.parent_id',
'locations.manager_id',
'locations.created_at',
'locations.updated_at',
'locations.image',
'locations.currency'
])
->withCount('assignedAssets')
->withCount('assets')
->withCount('users')->findOrFail($id);
return (new LocationsTransformer)->transformLocation($location);
}
@@ -122,7 +141,7 @@ class LocationsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Location::class);
$this->authorize('update', Location::class);
$location = Location::findOrFail($id);
$location->fill($request->all());

View File

@@ -39,7 +39,7 @@ class ManufacturersController extends Controller
$offset = request('offset', 0);
$offset = (($manufacturers) && (request('offset') > $manufacturers->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -83,7 +83,7 @@ class ManufacturersController extends Controller
public function show($id)
{
$this->authorize('view', Manufacturer::class);
$manufacturer = Manufacturer::findOrFail($id);
$manufacturer = Manufacturer::withCount('assets')->withCount('licenses')->withCount('consumables')->withCount('accessories')->findOrFail($id);
return (new ManufacturersTransformer)->transformManufacturer($manufacturer);
}
@@ -99,7 +99,7 @@ class ManufacturersController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Manufacturer::class);
$this->authorize('update', Manufacturer::class);
$manufacturer = Manufacturer::findOrFail($id);
$manufacturer->fill($request->all());

View File

@@ -27,15 +27,20 @@ class ProfileController extends Controller
foreach ($checkoutRequests as $checkoutRequest) {
$results['rows'][] = [
'image' => $checkoutRequest->itemRequested()->present()->getImageUrl(),
'name' => $checkoutRequest->itemRequested()->present()->name(),
'type' => $checkoutRequest->itemType(),
'qty' => $checkoutRequest->quantity,
'location' => ($checkoutRequest->location()) ? $checkoutRequest->location()->name : null,
'expected_checkin' => Helper::getFormattedDateObject($checkoutRequest->itemRequested()->expected_checkin, 'datetime'),
'request_date' => Helper::getFormattedDateObject($checkoutRequest->created_at, 'datetime'),
];
// Make sure the asset and request still exist
if ($checkoutRequest && $checkoutRequest->itemRequested()) {
$results['rows'][] = [
'image' => $checkoutRequest->itemRequested()->present()->getImageUrl(),
'name' => $checkoutRequest->itemRequested()->present()->name(),
'type' => $checkoutRequest->itemType(),
'qty' => $checkoutRequest->quantity,
'location' => ($checkoutRequest->location()) ? $checkoutRequest->location()->name : null,
'expected_checkin' => Helper::getFormattedDateObject($checkoutRequest->itemRequested()->expected_checkin, 'datetime'),
'request_date' => Helper::getFormattedDateObject($checkoutRequest->created_at, 'datetime'),
];
}
}
return $results;
}

View File

@@ -18,7 +18,8 @@ class ReportsController extends Controller
*/
public function index(Request $request)
{
$this->authorize('reports.view');
$actionlogs = Actionlog::with('item', 'user', 'target','location');
if ($request->has('search')) {

View File

@@ -30,7 +30,7 @@ class StatuslabelsController extends Controller
$statuslabels = $statuslabels->TextSearch($request->input('search'));
}
$offset = $request->input('offset', 0);
$offset = (($statuslabels) && (request('offset') > $statuslabels->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -101,7 +101,7 @@ class StatuslabelsController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Statuslabel::class);
$this->authorize('update', Statuslabel::class);
$statuslabel = Statuslabel::findOrFail($id);
$request->except('deployable', 'pending','archived');
@@ -160,6 +160,7 @@ class StatuslabelsController extends Controller
public function getAssetCountByStatuslabel()
{
$this->authorize('view', Statuslabel::class);
$statuslabels = Statuslabel::with('assets')->groupBy('id')->withCount('assets')->get();
@@ -237,6 +238,8 @@ class StatuslabelsController extends Controller
*/
public function checkIfDeployable($id) {
$statuslabel = Statuslabel::findOrFail($id);
$this->authorize('view', Asset::class);
if ($statuslabel->getStatuslabelType()=='deployable') {
return '1';
}

View File

@@ -26,14 +26,14 @@ class SuppliersController extends Controller
$suppliers = Supplier::select(
array('id','name','address','address2','city','state','country','fax', 'phone','email','contact','created_at','updated_at','deleted_at','image','notes')
)->withCount('assets')->withCount('licenses')->withCount('accessories')->whereNull('deleted_at');
)->withCount('assets')->withCount('licenses')->withCount('accessories');
if ($request->has('search')) {
$suppliers = $suppliers->TextSearch($request->input('search'));
}
$offset = request('offset', 0);
$offset = (($suppliers) && (request('offset') > $suppliers->count())) ? 0 : request('offset', 0);
$limit = $request->input('limit', 50);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -93,7 +93,7 @@ class SuppliersController extends Controller
*/
public function update(Request $request, $id)
{
$this->authorize('edit', Supplier::class);
$this->authorize('update', Supplier::class);
$supplier = Supplier::findOrFail($id);
$supplier->fill($request->all());

View File

@@ -12,6 +12,7 @@ use App\Http\Requests\SaveUserRequest;
use App\Models\Asset;
use App\Http\Transformers\AssetsTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Http\Transformers\AccessoriesTransformer;
class UsersController extends Controller
{
@@ -51,6 +52,7 @@ class UsersController extends Controller
'users.phone',
'users.state',
'users.two_factor_enrolled',
'users.two_factor_optin',
'users.updated_at',
'users.username',
'users.zip',
@@ -85,7 +87,7 @@ class UsersController extends Controller
}
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$offset = request('offset', 0);
$offset = (($users) && (request('offset') > $users->count())) ? 0 : request('offset', 0);
$limit = request('limit', 20);
switch ($request->input('sort')) {
@@ -105,7 +107,7 @@ class UsersController extends Controller
'assets','accessories', 'consumables','licenses','groups','activated','created_at',
'two_factor_enrolled','two_factor_optin','last_login', 'assets_count', 'licenses_count',
'consumables_count', 'accessories_count', 'phone', 'address', 'city', 'state',
'country', 'zip'
'country', 'zip', 'id'
];
$sort = in_array($request->get('sort'), $allowed_columns) ? $request->get('sort') : 'first_name';
@@ -142,13 +144,12 @@ class UsersController extends Controller
'users.avatar',
'users.email',
]
);
)->where('show_in_list', '=', '1');
$users = Company::scopeCompanyables($users);
if ($request->has('search')) {
$users = $users->where('first_name', 'LIKE', '%'.$request->get('search').'%')
->orWhere('last_name', 'LIKE', '%'.$request->get('search').'%')
$users = $users->SimpleNameSearch($request->get('search'))
->orWhere('username', 'LIKE', '%'.$request->get('search').'%')
->orWhere('employee_num', 'LIKE', '%'.$request->get('search').'%');
}
@@ -191,7 +192,8 @@ class UsersController extends Controller
*/
public function store(SaveUserRequest $request)
{
$this->authorize('view', User::class);
$this->authorize('create', User::class);
$user = new User;
$user->fill($request->all());
@@ -199,6 +201,12 @@ class UsersController extends Controller
$user->password = bcrypt($request->get('password', $tmp_pass));
if ($user->save()) {
if ($request->has('groups')) {
$user->groups()->sync($request->input('groups'));
} else {
$user->groups()->sync(array());
}
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.create')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
@@ -230,7 +238,8 @@ class UsersController extends Controller
*/
public function update(SaveUserRequest $request, $id)
{
$this->authorize('edit', User::class);
$this->authorize('update', User::class);
$user = User::findOrFail($id);
$user->fill($request->all());
@@ -247,6 +256,13 @@ class UsersController extends Controller
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
if ($user->save()) {
if ($request->has('groups')) {
$user->groups()->sync($request->input('groups'));
} else {
$user->groups()->sync(array());
}
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update')));
}
@@ -289,10 +305,28 @@ class UsersController extends Controller
public function assets($id)
{
$this->authorize('view', User::class);
$assets = Asset::where('assigned_to', '=', $id)->with('model')->get();
$this->authorize('view', Asset::class);
$assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model')->get();
return (new AssetsTransformer)->transformAssets($assets, $assets->count());
}
/**
* Return JSON containing a list of accessories assigned to a user.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.6.14]
* @param $userId
* @return string JSON
*/
public function accessories($id)
{
$this->authorize('view', User::class);
$user = User::findOrFail($id);
$this->authorize('view', Accessory::class);
$accessories = $user->accessories;
return (new AccessoriesTransformer)->transformAccessories($accessories, $accessories->count());
}
/**
* Reset the user's two-factor status
*
@@ -304,7 +338,7 @@ class UsersController extends Controller
public function postTwoFactorReset(Request $request)
{
$this->authorize('edit', User::class);
$this->authorize('update', User::class);
if ($request->has('id')) {
try {

View File

@@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Http\Requests\AssetCheckinRequest;
use App\Models\Asset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AssetCheckinController extends Controller
{
/**
* Returns a view that presents a form to check an asset back into inventory.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @param string $backto
* @since [v1.0]
* @return View
*/
public function create($assetId, $backto = null)
{
// Check if the asset exists
if (is_null($asset = Asset::find($assetId))) {
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('checkin', $asset);
return view('hardware/checkin', compact('asset'))->with('statusLabel_list', Helper::statusLabelList())->with('backto', $backto);
}
/**
* Validate and process the form data to check an asset back into inventory.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param AssetCheckinRequest $request
* @param int $assetId
* @param null $backto
* @return Redirect
* @since [v1.0]
*/
public function store(AssetCheckinRequest $request, $assetId = null, $backto = null)
{
// Check if the asset exists
if (is_null($asset = Asset::find($assetId))) {
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('checkin', $asset);
if ($asset->assignedType() == Asset::USER) {
$user = $asset->assignedTo;
}
if (is_null($target = $asset->assignedTo)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.already_checked_in'));
}
$asset->expected_checkin = null;
$asset->last_checkout = null;
$asset->assigned_to = null;
$asset->assignedTo()->disassociate($asset);
$asset->assigned_type = null;
$asset->accepted = null;
$asset->name = e($request->get('name'));
if ($request->has('status_id')) {
$asset->status_id = e($request->get('status_id'));
}
$asset->location_id = $asset->rtd_location_id;
if ($request->has('location_id')) {
$asset->location_id = e($request->get('location_id'));
}
// Was the asset updated?
if ($asset->save()) {
$logaction = $asset->logCheckin($target, e(request('note')));
$data['log_id'] = $logaction->id;
$data['first_name'] = get_class($target) == User::class ? $target->first_name : '';
$data['last_name'] = get_class($target) == User::class ? $target->last_name : '';
$data['item_name'] = $asset->present()->name();
$data['checkin_date'] = $logaction->created_at;
$data['item_tag'] = $asset->asset_tag;
$data['item_serial'] = $asset->serial;
$data['note'] = $logaction->note;
$data['manufacturer_name'] = $asset->model->manufacturer->name;
$data['model_name'] = $asset->model->name;
$data['model_number'] = $asset->model->model_number;
if ($backto=='user') {
return redirect()->route("users.show", $user->id)->with('success', trans('admin/hardware/message.checkin.success'));
}
return redirect()->route("hardware.index")->with('success', trans('admin/hardware/message.checkin.success'));
}
// Redirect to the asset management page with error
return redirect()->route("hardware.index")->with('error', trans('admin/hardware/message.checkin.error'));
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\CheckoutNotAllowed;
use App\Http\Controllers\CheckInOutRequest;
use App\Http\Requests\AssetCheckoutRequest;
use App\Models\Asset;
use App\Models\Location;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AssetCheckoutController extends Controller
{
use CheckInOutRequest;
/**
* Returns a view that presents a form to check an asset out to a
* user.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return View
*/
public function create($assetId)
{
// Check if the asset exists
if (is_null($asset = Asset::find(e($assetId)))) {
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('checkout', $asset);
if ($asset->availableForCheckout()) {
return view('hardware/checkout', compact('asset'));
}
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkout.not_available'));
// Get the dropdown of users and then pass it to the checkout view
}
/**
* Validate and process the form data to check out an asset to a user.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param AssetCheckoutRequest $request
* @param int $assetId
* @return Redirect
* @since [v1.0]
*/
public function store(AssetCheckoutRequest $request, $assetId)
{
try {
// Check if the asset exists
if (!$asset = Asset::find($assetId)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
} elseif (!$asset->availableForCheckout()) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkout.not_available'));
}
$this->authorize('checkout', $asset);
$admin = Auth::user();
$target = $this->determineCheckoutTarget($asset);
if ($asset->is($target)) {
throw new CheckoutNotAllowed('You cannot check an asset out to itself.');
}
$asset = $this->updateAssetLocation($asset, $target);
$checkout_at = date("Y-m-d H:i:s");
if (($request->has('checkout_at')) && ($request->get('checkout_at')!= date("Y-m-d"))) {
$checkout_at = $request->get('checkout_at');
}
$expected_checkin = '';
if ($request->has('expected_checkin')) {
$expected_checkin = $request->get('expected_checkin');
}
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), $request->get('name'))) {
return redirect()->route("hardware.index")->with('success', trans('admin/hardware/message.checkout.success'));
}
// Redirect to the asset management page with error
return redirect()->to("hardware/$assetId/checkout")->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($asset->getErrors());
} catch (ModelNotFoundException $e) {
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($asset->getErrors());
} catch (CheckoutNotAllowed $e) {
return redirect()->back()->with('error', $e->getMessage());
}
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Http\Requests\AssetFileRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
class AssetFilesController extends Controller
{
/**
* Upload a file to the server.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param AssetFileRequest $request
* @param int $assetId
* @return Redirect
* @since [v1.0]
*/
public function store(AssetFileRequest $request, $assetId = null)
{
if (!$asset = Asset::find($assetId)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('update', $asset);
$destinationPath = config('app.private_uploads').'/assets';
if ($request->hasFile('file')) {
foreach ($request->file('file') as $file) {
$extension = $file->getClientOriginalExtension();
$filename = 'hardware-'.$asset->id.'-'.str_random(8);
$filename .= '-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
$file->move($destinationPath, $filename);
$asset->logUpload($filename, e($request->get('notes')));
}
return redirect()->back()->with('success', trans('admin/hardware/message.upload.success'));
}
return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles'));
}
/**
* Check for permissions and display the file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @param int $fileId
* @since [v1.0]
* @return View
*/
public function show($assetId = null, $fileId = null, $download = true)
{
$asset = Asset::find($assetId);
// the asset is valid
if (isset($asset->id)) {
$this->authorize('view', $asset);
if (!$log = Actionlog::find($fileId)) {
return response('No matching record for that asset/file', 500)
->header('Content-Type', 'text/plain');
}
$file = $log->get_src('assets');
if ($log->action_type =='audit') {
$file = $log->get_src('audits');
}
if (!file_exists($file)) {
return response('File '.$file.' not found on server', 404)
->header('Content-Type', 'text/plain');
}
if ($download != 'true') {
if ($contents = file_get_contents($file)) {
return Response::make($contents)->header('Content-Type', mime_content_type($file));
}
return JsonResponse::create(["error" => "Failed validation: "], 500);
}
return Response::download($file);
}
// Prepare the error message
$error = trans('admin/hardware/message.does_not_exist', ['id' => $fileId]);
// Redirect to the hardware management page
return redirect()->route('hardware.index')->with('error', $error);
}
/**
* Delete the associated file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @param int $fileId
* @since [v1.0]
* @return View
*/
public function destroy($assetId = null, $fileId = null)
{
$asset = Asset::find($assetId);
$this->authorize('update', $asset);
$destinationPath = config('app.private_uploads').'/imports/assets';
// the asset is valid
if (isset($asset->id)) {
$this->authorize('update', $asset);
$log = Actionlog::find($fileId);
$full_filename = $destinationPath.'/'.$log->filename;
if (file_exists($full_filename)) {
unlink($destinationPath.'/'.$log->filename);
}
$log->delete();
return redirect()->back()->with('success', trans('admin/hardware/message.deletefile.success'));
}
// Redirect to the hardware management page
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
}

View File

@@ -162,6 +162,9 @@ class AssetMaintenancesController extends Controller
// Redirect to the improvement management page
return redirect()->route('maintenances.index')
->with('error', trans('admin/asset_maintenances/message.not_found'));
} elseif (!$assetMaintenance->asset) {
return redirect()->route('maintenances.index')
->with('error', 'The asset associated with this maintenance does not exist.');
} elseif (!Company::isCurrentUserHasAccess($assetMaintenance->asset)) {
return static::getInsufficientPermissionsRedirect();
}

View File

@@ -120,46 +120,6 @@ class AssetModelsController extends Controller
return redirect()->back()->withInput()->withErrors($model->getErrors());
}
/**
* Validates and stores new Asset Model data created from the
* modal form on the Asset Creation view.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v2.0]
* @param Request $request
* @return String JSON
*/
public function apiStore(Request $request)
{
//COPYPASTA!!!! FIXME
$this->authorize('create', AssetModel::class);
$model = new AssetModel;
$settings=Input::all();
$settings['eol']= null;
$model->name=$request->input('name');
$model->manufacturer_id = $request->input('manufacturer_id');
$model->category_id = $request->input('category_id');
$model->model_number = $request->input('model_number');
$model->user_id = Auth::id();
$model->notes = $request->input('notes');
$model->eol= null;
if ($request->input('fieldset_id')=='') {
$model->fieldset_id = null;
} else {
$model->fieldset_id = e($request->input('fieldset_id'));
}
if ($model->save()) {
return JsonResponse::create($model);
} else {
return JsonResponse::create(["error" => "Failed validation: ".print_r($model->getErrors()->all('<li>:message</li>'), true)], 500);
}
}
/**
* Returns a view containing the asset model edit form.
*
@@ -249,7 +209,7 @@ class AssetModelsController extends Controller
try {
unlink(app('models_upload_path').$old_image);
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}
@@ -286,7 +246,7 @@ class AssetModelsController extends Controller
try {
unlink(public_path().'/uploads/models/'.$model->image);
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}
@@ -372,9 +332,7 @@ class AssetModelsController extends Controller
// Show the page
$view = View::make('models/edit');
$view->with('category_list', Helper::categoryList('asset'));
$view->with('depreciation_list', Helper::depreciationList());
$view->with('manufacturer_list', Helper::manufacturerList());
$view->with('item', $model);
$view->with('clone_model', $model_to_clone);
return $view;
@@ -408,7 +366,7 @@ class AssetModelsController extends Controller
*/
public function postBulkEdit(Request $request)
{
$models_raw_array = Input::get('ids');
// Make sure some IDs have been selected
@@ -433,13 +391,8 @@ class AssetModelsController extends Controller
$nochange = ['NC' => 'No Change'];
$fieldset_list = $nochange + Helper::customFieldsetList();
$depreciation_list = $nochange + Helper::depreciationList();
$category_list = $nochange + Helper::categoryList('asset');
$manufacturer_list = $nochange + Helper::manufacturerList();
return view('models/bulk-edit', compact('models'))
->with('manufacturer_list', $manufacturer_list)
->with('category_list', $category_list)
->with('fieldset_list', $fieldset_list)
->with('depreciation_list', $depreciation_list);
}

View File

@@ -60,14 +60,14 @@ class AssetsController extends Controller
}
/**
* Returns a view that invokes the ajax tables which actually contains
* the content for the assets listing, which is generated in getDatatable.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @see AssetController::getDatatable() method that generates the JSON response
* @since [v1.0]
* @return View
*/
* Returns a view that invokes the ajax tables which actually contains
* the content for the assets listing, which is generated in getDatatable.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @see AssetController::getDatatable() method that generates the JSON response
* @since [v1.0]
* @return View
*/
public function index(Request $request)
{
$this->authorize('index', Asset::class);
@@ -79,24 +79,6 @@ class AssetsController extends Controller
return view('hardware/index')->with('company', $company);
}
/**
* Searches the assets table by asset tag, and redirects if it finds one
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @return Redirect
*/
public function getAssetByTag()
{
$topsearch = (Input::get('topsearch')=="true");
if (!$asset = Asset::where('asset_tag', '=', Input::get('assetTag'))->first()) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('view', $asset);
return redirect()->route('hardware.show', $asset->id)->with('topsearch', $topsearch);
}
/**
* Returns a view that presents a form to create a new asset.
*
@@ -110,12 +92,8 @@ class AssetsController extends Controller
{
$this->authorize('create', Asset::class);
$view = View::make('hardware/edit')
->with('supplier_list', Helper::suppliersList())
->with('model_list', Helper::modelList())
->with('statuslabel_list', Helper::statusLabelList())
->with('item', new Asset)
->with('manufacturer', Helper::manufacturerList()) //handled in modal now?
->with('category', Helper::categoryList('asset')) //handled in modal now?
->with('statuslabel_types', Helper::statusTypeList());
if ($request->has('model_id')) {
@@ -126,12 +104,12 @@ class AssetsController extends Controller
}
/**
* Validate and process new asset form data.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @return Redirect
*/
* Validate and process new asset form data.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @return Redirect
*/
public function store(AssetRequest $request)
{
$this->authorize(Asset::class);
@@ -153,7 +131,7 @@ class AssetsController extends Controller
$asset->depreciate = '0';
$asset->status_id = request('status_id', 0);
$asset->warranty_months = request('warranty_months', null);
$asset->purchase_cost = Helper::ParseFloat(Input::get('purchase_cost'));
$asset->purchase_cost = Helper::ParseFloat($request->get('purchase_cost'));
$asset->purchase_date = request('purchase_date', null);
$asset->assigned_to = request('assigned_to', null);
$asset->supplier_id = request('supplier_id', 0);
@@ -217,7 +195,7 @@ class AssetsController extends Controller
}
}
// Was the asset created?
// Was the asset created?
if ($asset->save()) {
@@ -233,7 +211,7 @@ class AssetsController extends Controller
}
if (isset($target)) {
$asset->checkOut($target, Auth::user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e(Input::get('name')), $location);
$asset->checkOut($target, Auth::user(), date('Y-m-d H:i:s'), '', 'Checked out on asset creation', e($request->get('name')), $location);
}
// Redirect to the asset listing page
\Session::flash('success', trans('admin/hardware/message.create.success'));
@@ -245,13 +223,13 @@ class AssetsController extends Controller
}
/**
* Returns a view that presents a form to edit an existing asset.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return View
*/
* Returns a view that presents a form to edit an existing asset.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return View
*/
public function edit($assetId = null)
{
if (!$item = Asset::find($assetId)) {
@@ -262,20 +240,62 @@ class AssetsController extends Controller
$this->authorize($item);
return view('hardware/edit', compact('item'))
->with('model_list', Helper::modelList())
->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList());
}
/**
* Validate and process asset edit form.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return Redirect
*/
* Returns a view that presents information about an asset for detail view.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return View
*/
public function show($assetId = null)
{
$asset = Asset::withTrashed()->find($assetId);
$this->authorize('view', $asset);
$settings = Setting::getSettings();
if (isset($asset)) {
$audit_log = Actionlog::where('action_type', '=', 'audit')
->where('item_id', '=', $assetId)
->where('item_type', '=', Asset::class)
->orderBy('created_at', 'DESC')->first();
if ($asset->location) {
$use_currency = $asset->location->currency;
} else {
if ($settings->default_currency!='') {
$use_currency = $settings->default_currency;
} else {
$use_currency = trans('general.currency');
}
}
$qr_code = (object) array(
'display' => $settings->qr_code == '1',
'url' => route('qr_code/hardware', $asset->id)
);
return view('hardware/view', compact('asset', 'qr_code', 'settings'))
->with('use_currency', $use_currency)->with('audit_log', $audit_log);
}
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
/**
* Validate and process asset edit form.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return Redirect
*/
public function update(AssetRequest $request, $assetId = null)
{
@@ -306,7 +326,7 @@ class AssetsController extends Controller
unlink(public_path().'/uploads/assets/'.$asset->image);
$asset->image = '';
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}
@@ -323,7 +343,7 @@ class AssetsController extends Controller
$asset->physical = '1';
// Update the image
if (Input::has('image')) {
if ($request->has('image')) {
$image = $request->input('image');
// See postCreate for more explaination of the following.
$header = explode(';', $image, 2)[0];
@@ -384,13 +404,13 @@ class AssetsController extends Controller
}
/**
* Delete a given asset (mark as deleted).
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return Redirect
*/
* Delete a given asset (mark as deleted).
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return Redirect
*/
public function destroy($assetId)
{
// Check if the asset exists
@@ -410,256 +430,33 @@ class AssetsController extends Controller
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.delete.success'));
}
/**
* Returns a view that presents a form to check an asset out to a
* user.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return View
*/
public function getCheckout($assetId)
{
// Check if the asset exists
if (is_null($asset = Asset::find(e($assetId)))) {
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('checkout', $asset);
if ($asset->availableForCheckout()) {
return view('hardware/checkout', compact('asset'));
}
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkout.not_available'));
// Get the dropdown of users and then pass it to the checkout view
}
/**
* Validate and process the form data to check out an asset to a user.
* Searches the assets table by asset tag, and redirects if it finds one
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param AssetCheckoutRequest $request
* @param int $assetId
* @since [v3.0]
* @return Redirect
* @since [v1.0]
*/
public function postCheckout(AssetCheckoutRequest $request, $assetId)
public function getAssetByTag(Request $request)
{
// Check if the asset exists
if (!$asset = Asset::find($assetId)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
} elseif (!$asset->availableForCheckout()) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkout.not_available'));
}
$this->authorize('checkout', $asset);
$admin = Auth::user();
$topsearch = ($request->get('topsearch')=="true");
// This item is checked out to a location
if (request('checkout_to_type')=='location') {
$target = Location::find(request('assigned_location'));
$asset->location_id = ($target) ? $target->id : '';
} elseif (request('checkout_to_type')=='asset') {
if (request('assigned_asset') == $assetId) {
return redirect()->back()->with('error', 'You cannot check an asset out to itself.');
}
$target = Asset::where('id','!=',$assetId)->find(request('assigned_asset'));
$asset->location_id = $target->rtd_location_id;
// Override with the asset's location_id if it has one
if ($target->location_id!='') {
$asset->location_id = ($target) ? $target->location_id : '';
}
} elseif (request('checkout_to_type')=='user') {
// Fetch the target and set the asset's new location_id
$target = User::find(request('assigned_user'));
$asset->location_id = ($target) ? $target->location_id : '';
}
// No valid target was found - error out
if (!$target) {
return redirect()->back()->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($asset->getErrors());
}
if ((Input::has('checkout_at')) && (Input::get('checkout_at')!= date("Y-m-d"))) {
$checkout_at = Input::get('checkout_at');
} else {
$checkout_at = date("Y-m-d H:i:s");
}
if (Input::has('expected_checkin')) {
$expected_checkin = Input::get('expected_checkin');
} else {
$expected_checkin = '';
}
if ($asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e(Input::get('note')), Input::get('name'))) {
return redirect()->route("hardware.index")->with('success', trans('admin/hardware/message.checkout.success'));
}
// Redirect to the asset management page with error
return redirect()->to("hardware/$assetId/checkout")->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($asset->getErrors());
}
/**
* Returns a view that presents a form to check an asset back into inventory.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @param string $backto
* @since [v1.0]
* @return View
*/
public function getCheckin($assetId, $backto = null)
{
// Check if the asset exists
if (is_null($asset = Asset::find($assetId))) {
// Redirect to the asset management page with error
if (!$asset = Asset::where('asset_tag', '=', $request->get('assetTag'))->first()) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('checkin', $asset);
return view('hardware/checkin', compact('asset'))->with('statusLabel_list', Helper::statusLabelList())->with('backto', $backto);
}
/**
* Validate and process the form data to check an asset back into inventory.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param AssetCheckinRequest $request
* @param int $assetId
* @param null $backto
* @return Redirect
* @since [v1.0]
*/
public function postCheckin(AssetCheckinRequest $request, $assetId = null, $backto = null)
{
// Check if the asset exists
if (is_null($asset = Asset::find($assetId))) {
// Redirect to the asset management page with error
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('checkin', $asset);
$admin = Auth::user();
if ($asset->assignedType() == Asset::USER) {
$user = $asset->assignedTo;
}
if (is_null($target = $asset->assignedTo)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.already_checked_in'));
}
$asset->expected_checkin = null;
$asset->last_checkout = null;
$asset->assigned_to = null;
$asset->assignedTo()->disassociate($asset);
$asset->assigned_type = null;
$asset->accepted = null;
$asset->name = e(Input::get('name'));
if (Input::has('status_id')) {
$asset->status_id = e(Input::get('status_id'));
}
$asset->location_id = $asset->rtd_location_id;
if (Input::has('location_id')) {
$asset->location_id = e(Input::get('location_id'));
}
// Was the asset updated?
if ($asset->save()) {
$logaction = $asset->logCheckin($target, e(request('note')));
$data['log_id'] = $logaction->id;
$data['first_name'] = get_class($target) == User::class ? $target->first_name : '';
$data['last_name'] = get_class($target) == User::class ? $target->last_name : '';
$data['item_name'] = $asset->present()->name();
$data['checkin_date'] = $logaction->created_at;
$data['item_tag'] = $asset->asset_tag;
$data['item_serial'] = $asset->serial;
$data['note'] = $logaction->note;
$data['manufacturer_name'] = $asset->model->manufacturer->name;
$data['model_name'] = $asset->model->name;
$data['model_number'] = $asset->model->model_number;
if ($backto=='user') {
return redirect()->route("users.show", $user->id)->with('success', trans('admin/hardware/message.checkin.success'));
}
return redirect()->route("hardware.index")->with('success', trans('admin/hardware/message.checkin.success'));
}
// Redirect to the asset management page with error
return redirect()->route("hardware.index")->with('error', trans('admin/hardware/message.checkin.error'));
}
/**
* Returns a view that presents information about an asset for detail view.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return View
*/
public function show($assetId = null)
{
$asset = Asset::withTrashed()->find($assetId);
$this->authorize('view', $asset);
$settings = Setting::getSettings();
if (isset($asset)) {
$audit_log = Actionlog::where('action_type', '=', 'audit')
->where('item_id', '=', $assetId)
->where('item_type', '=', Asset::class)
->orderBy('created_at', 'DESC')->first();
if ($asset->location) {
$use_currency = $asset->location->currency;
} else {
if ($settings->default_currency!='') {
$use_currency = $settings->default_currency;
} else {
$use_currency = trans('general.currency');
}
}
$qr_code = (object) array(
'display' => $settings->qr_code == '1',
'url' => route('qr_code/hardware', $asset->id)
);
return view('hardware/view', compact('asset', 'qr_code', 'settings'))
->with('use_currency', $use_currency)->with('audit_log', $audit_log);
}
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
return redirect()->route('hardware.show', $asset->id)->with('topsearch', $topsearch);
}
/**
* Return a QR code for the asset
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return Response
*/
* Return a QR code for the asset
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return Response
*/
public function getQrCode($assetId = null)
{
$settings = Setting::getSettings();
@@ -719,13 +516,13 @@ class AssetsController extends Controller
}
/**
* Returns a view that presents a form to clone an asset.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return View
*/
* Returns a view that presents a form to clone an asset.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return View
*/
public function getClone($assetId = null)
{
// Check if the asset exists
@@ -743,9 +540,9 @@ class AssetsController extends Controller
$asset->assigned_to = '';
return view('hardware/edit')
->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList())
->with('item', $asset);
->with('statuslabel_list', Helper::statusLabelList())
->with('statuslabel_types', Helper::statusTypeList())
->with('item', $asset);
}
/**
@@ -844,15 +641,15 @@ class AssetsController extends Controller
$item[$asset_tag][$batch_counter]['user_id'] = $user->id;
Actionlog::firstOrCreate(array(
'item_id' => $asset->id,
'item_type' => Asset::class,
'user_id' => Auth::user()->id,
'note' => 'Checkout imported by '.Auth::user()->present()->fullName().' from history importer',
'target_id' => $item[$asset_tag][$batch_counter]['user_id'],
'target_type' => User::class,
'created_at' => $item[$asset_tag][$batch_counter]['checkout_date'],
'action_type' => 'checkout',
));
'item_id' => $asset->id,
'item_type' => Asset::class,
'user_id' => Auth::user()->id,
'note' => 'Checkout imported by '.Auth::user()->present()->fullName().' from history importer',
'target_id' => $item[$asset_tag][$batch_counter]['user_id'],
'target_type' => User::class,
'created_at' => $item[$asset_tag][$batch_counter]['checkout_date'],
'action_type' => 'checkout',
));
$asset->assigned_to = $user->id;
@@ -885,14 +682,14 @@ class AssetsController extends Controller
$asset_batch[$x]['real_checkin'] = $checkin_date;
Actionlog::firstOrCreate(array(
'item_id' => $asset_batch[$x]['asset_id'],
'item_type' => Asset::class,
'user_id' => Auth::user()->id,
'note' => 'Checkin imported by ' . Auth::user()->present()->fullName() . ' from history importer',
'target_id' => null,
'created_at' => $checkin_date,
'action_type' => 'checkin'
));
'item_id' => $asset_batch[$x]['asset_id'],
'item_type' => Asset::class,
'user_id' => Auth::user()->id,
'note' => 'Checkin imported by ' . Auth::user()->present()->fullName() . ' from history importer',
'target_id' => null,
'created_at' => $checkin_date,
'action_type' => 'checkin'
));
}
}
}
@@ -901,13 +698,13 @@ class AssetsController extends Controller
}
/**
* Retore a deleted asset.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return View
*/
* Retore a deleted asset.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @since [v1.0]
* @return View
*/
public function getRestore($assetId = null)
{
// Get asset information
@@ -929,355 +726,6 @@ class AssetsController extends Controller
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
/**
* Upload a file to the server.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param AssetFileRequest $request
* @param int $assetId
* @return Redirect
* @since [v1.0]
*/
public function postUpload(AssetFileRequest $request, $assetId = null)
{
if (!$asset = Asset::find($assetId)) {
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
$this->authorize('update', $asset);
$destinationPath = config('app.private_uploads').'/assets';
if ($request->hasFile('assetfile')) {
foreach ($request->file('assetfile') as $file) {
$extension = $file->getClientOriginalExtension();
$filename = 'hardware-'.$asset->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
$file->move($destinationPath, $filename);
$asset->logUpload($filename, e(Input::get('notes')));
}
return redirect()->back()->with('success', trans('admin/hardware/message.upload.success'));
}
return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles'));
}
/**
* Delete the associated file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @param int $fileId
* @since [v1.0]
* @return View
*/
public function deleteFile($assetId = null, $fileId = null)
{
$asset = Asset::find($assetId);
$this->authorize('update', $asset);
$destinationPath = config('app.private_uploads').'/imports/assets';
// the asset is valid
if (isset($asset->id)) {
$this->authorize('update', $asset);
$log = Actionlog::find($fileId);
$full_filename = $destinationPath.'/'.$log->filename;
if (file_exists($full_filename)) {
unlink($destinationPath.'/'.$log->filename);
}
$log->delete();
return redirect()->back()->with('success', trans('admin/hardware/message.deletefile.success'));
}
// Redirect to the hardware management page
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));
}
/**
* Check for permissions and display the file.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $assetId
* @param int $fileId
* @since [v1.0]
* @return View
*/
public function displayFile($assetId = null, $fileId = null, $download = true)
{
$asset = Asset::find($assetId);
if (isset($asset->id)) {
$this->authorize('view', $asset);
if (!$log = Actionlog::find($fileId)) {
return response('No matching record for that asset/file', 500)
->header('Content-Type', 'text/plain');
}
$file = $log->get_src('assets');
if ($log->action_type =='audit') {
$file = $log->get_src('audits');
}
if (!file_exists($file)) {
return response('File '.$file.' not found on server', 404)
->header('Content-Type', 'text/plain');
}
if ($download != 'true') {
if ($contents = file_get_contents($file)) {
return Response::make($contents)->header('Content-Type', $filetype);
}
return JsonResponse::create(["error" => "Failed validation: "], 500);
}
return Response::download($file);
}
// Redirect to the hardware management page
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist', compact('id')));
}
/**
* Display the bulk edit page.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @return View
* @internal param int $assetId
* @since [v2.0]
*/
public function postBulkEdit(Request $request)
{
$this->authorize('update', Asset::class);
if (!$request->has('ids')) {
return redirect()->back()->with('error', 'No assets selected');
}
$asset_raw_array = $request->input('ids');
foreach ($asset_raw_array as $asset_id => $value) {
$asset_ids[] = $asset_id;
}
if ($request->has('bulk_actions')) {
if ($request->input('bulk_actions')=='labels') {
$count = 0;
return view('hardware/labels')
->with('assets', Asset::find($asset_ids))
->with('settings', Setting::getSettings())
->with('count', $count);
} elseif ($request->input('bulk_actions')=='delete') {
$assets = Asset::with('assignedTo', 'location')->find($asset_ids);
$assets->each(function ($asset) {
$this->authorize('delete', $asset);
});
return view('hardware/bulk-delete')->with('assets', $assets);
// Bulk edit
} elseif ($request->input('bulk_actions')=='edit') {
return view('hardware/bulk')
->with('assets', request('ids'))
->with('statuslabel_list', Helper::statusLabelList())
->with(
'companies_list',
array('' => '') + array('clear' => trans('general.remove_company')) + Helper::companyList()
);
}
}
return redirect()->back()->with('error', 'No action selected');
}
/**
* Save bulk edits
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @return Redirect
* @internal param array $assets
* @since [v2.0]
*/
public function postBulkSave(Request $request)
{
$this->authorize('update', Asset::class);
\Log::debug($request->input('ids'));
if (($request->has('ids')) && (count($request->input('ids')) > 0)) {
$assets = $request->input('ids');
if (($request->has('purchase_date'))
|| ($request->has('purchase_cost'))
|| ($request->has('supplier_id'))
|| ($request->has('order_number'))
|| ($request->has('warranty_months'))
|| ($request->has('rtd_location_id'))
|| ($request->has('requestable'))
|| ($request->has('company_id'))
|| ($request->has('status_id'))
|| ($request->has('model_id'))
) {
foreach ($assets as $key => $value) {
$update_array = array();
if ($request->has('purchase_date')) {
$update_array['purchase_date'] = $request->input('purchase_date');
}
if ($request->has('purchase_cost')) {
$update_array['purchase_cost'] = Helper::ParseFloat($request->input('purchase_cost'));
}
if ($request->has('supplier_id')) {
$update_array['supplier_id'] = $request->input('supplier_id');
}
if ($request->has('model_id')) {
$update_array['model_id'] = $request->input('model_id');
}
if ($request->has('company_id')) {
if ($request->input('company_id')=="clear") {
$update_array['company_id'] = null;
} else {
$update_array['company_id'] = $request->input('company_id');
}
}
if ($request->has('order_number')) {
$update_array['order_number'] = $request->input('order_number');
}
if ($request->has('warranty_months')) {
$update_array['warranty_months'] = $request->input('warranty_months');
}
if ($request->has('rtd_location_id')) {
$update_array['rtd_location_id'] = $request->input('rtd_location_id');
if (($request->has('update_real_loc'))
&& (($request->input('update_real_loc')) == '1'))
{
$update_array['location_id'] = $request->input('rtd_location_id');
}
}
if ($request->has('status_id')) {
$update_array['status_id'] = $request->input('status_id');
}
if ($request->has('requestable')) {
$update_array['requestable'] = $request->input('requestable');
}
DB::table('assets')
->where('id', $key)
->update($update_array);
} // endforeach
return redirect()->route("hardware.index")->with('success', trans('admin/hardware/message.update.success'));
// no values given, nothing to update
}
return redirect()->route("hardware.index")->with('warning', trans('admin/hardware/message.update.nothing_updated'));
} // endif
return redirect()->route("hardware.index")->with('warning', trans('No assets selected, so nothing was updated.'));
}
/**
* Save bulk deleted.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @return View
* @internal param array $assets
* @since [v2.0]
*/
public function postBulkDelete()
{
$this->authorize('delete', Asset::class);
if (Input::has('ids')) {
$assets = Asset::find(Input::get('ids'));
foreach ($assets as $asset) {
$update_array['deleted_at'] = date('Y-m-d H:i:s');
$update_array['assigned_to'] = null;
DB::table('assets')
->where('id', $asset->id)
->update($update_array);
} // endforeach
return redirect()->to("hardware")->with('success', trans('admin/hardware/message.delete.success'));
// no values given, nothing to update
}
return redirect()->to("hardware")->with('info', trans('admin/hardware/message.delete.nothing_updated'));
}
public function getBulkCheckout()
{
$this->authorize('checkout', Asset::class);
// Filter out assets that are not deployable.
return view('hardware/bulk-checkout')
->with('users_list', Helper::usersList());
}
public function postBulkCheckout(Request $request)
{
$admin = Auth::user();
// Find checkout to type
if (request('checkout_to_type')=='location') {
$target = Location::find(request('assigned_location'));
} elseif (request('checkout_to_type')=='asset') {
$target = Asset::find(request('assigned_asset'));
} elseif (request('checkout_to_type')=='user') {
$target = User::find(request('assigned_user'));
}
if (!is_array(Input::get('selected_assets'))) {
return redirect()->route('hardware/bulkcheckout')->withInput()->with('error', trans('admin/hardware/message.checkout.no_assets_selected'));
}
$asset_ids = array_filter(Input::get('selected_assets'));
foreach ($asset_ids as $asset_id) {
if ($target->id == $asset_id) {
return redirect()->back()->with('error', 'You cannot check an asset out to itself.');
}
}
if ((Input::has('checkout_at')) && (Input::get('checkout_at')!= date("Y-m-d"))) {
$checkout_at = e(Input::get('checkout_at'));
} else {
$checkout_at = date("Y-m-d H:i:s");
}
if (Input::has('expected_checkin')) {
$expected_checkin = e(Input::get('expected_checkin'));
} else {
$expected_checkin = '';
}
$errors = [];
DB::transaction(function () use ($target, $admin, $checkout_at, $expected_checkin, $errors, $asset_ids) {
foreach ($asset_ids as $asset_id) {
$asset = Asset::find($asset_id);
$this->authorize('checkout', $asset);
$error = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e(Input::get('note')), null);
if ($target->location_id!='') {
$asset->location_id = $target->location_id;
$asset->unsetEventDispatcher();
$asset->save();
}
if ($error) {
array_merge_recursive($errors, $asset->getErrors()->toArray());
}
}
});
if (!$errors) {
// Redirect to the new asset page
return redirect()->to("hardware")->with('success', trans('admin/hardware/message.checkout.success'));
}
// Redirect to the asset management page with error
return redirect()->to("hardware/bulk-checkout")->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($errors);
}
public function quickScan()
{
$this->authorize('audit', Asset::class);
@@ -1296,6 +744,18 @@ class AssetsController extends Controller
return view('hardware/audit')->with('asset', $asset)->with('next_audit_date', $dt)->with('locations_list');
}
public function dueForAudit()
{
$this->authorize('audit', Asset::class);
return view('hardware/audit-due');
}
public function overdueForAudit()
{
$this->authorize('audit', Asset::class);
return view('hardware/audit-overdue');
}
public function auditStore(AssetFileRequest $request, $id)
{
@@ -1327,14 +787,14 @@ class AssetsController extends Controller
if ($request->hasFile('image')) {
$file = $request->file('image');
try {
$destinationPath = config('app.private_uploads').'/audits';
$extension = $file->getClientOriginalExtension();
$filename = 'audit-'.$asset->id.'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
$file->move($destinationPath, $filename);
} catch (\Exception $e) {
\Log::error($e);
}
try {
$destinationPath = config('app.private_uploads').'/audits';
$extension = $file->getClientOriginalExtension();
$filename = 'audit-'.$asset->id.'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
$file->move($destinationPath, $filename);
} catch (\Exception $e) {
\Log::info($e);
}
}
$asset->logAudit($request->input('note'), $request->input('location_id'), $filename);

View File

@@ -55,7 +55,10 @@ class ForgotPasswordController extends Controller
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$request->only('email')
array_merge(
$request->only('email'),
['activated' => '1']
)
);
if ($response === \Password::RESET_LINK_SENT) {

View File

@@ -68,48 +68,54 @@ class LoginController extends Controller
{
$remote_user = $request->server('REMOTE_USER');
if (Setting::getSettings()->login_remote_user_enabled == "1" && isset($remote_user) && !empty($remote_user)) {
LOG::debug("Authenticatiing via REMOTE_USER.");
Log::debug("Authenticatiing via REMOTE_USER.");
$pos = strpos($remote_user, '\\');
if ($pos > 0) {
$remote_user = substr($remote_user, $pos + 1);
};
try {
$user = User::where('username', '=', $remote_user)->whereNull('deleted_at')->first();
LOG::debug("Remote user auth lookup complete");
$user = User::where('username', '=', $remote_user)->whereNull('deleted_at')->where('activated', '=', '1')->first();
Log::debug("Remote user auth lookup complete");
if(!is_null($user)) Auth::login($user, true);
} catch(Exception $e) {
LOG::error("There was an error authenticating the Remote user: " . $e->getMessage());
Log::debug("There was an error authenticating the Remote user: " . $e->getMessage());
}
}
}
private function loginViaLdap(Request $request)
{
LOG::debug("Binding user to LDAP.");
Log::debug("Binding user to LDAP.");
$ldap_user = Ldap::findAndBindUserLdap($request->input('username'), $request->input('password'));
if (!$ldap_user) {
LOG::debug("LDAP user ".$request->input('username')." not found in LDAP or could not bind");
Log::debug("LDAP user ".$request->input('username')." not found in LDAP or could not bind");
throw new \Exception("Could not find user in LDAP directory");
} else {
LOG::debug("LDAP user ".$request->input('username')." successfully bound to LDAP");
Log::debug("LDAP user ".$request->input('username')." successfully bound to LDAP");
}
// Check if the user already exists in the database and was imported via LDAP
$user = User::where('username', '=', Input::get('username'))->whereNull('deleted_at')->where('ldap_import', '=', 1)->first();
LOG::debug("Local auth lookup complete");
$user = User::where('username', '=', Input::get('username'))->whereNull('deleted_at')->where('ldap_import', '=', 1)->where('activated', '=', '1')->first();
Log::debug("Local auth lookup complete");
// The user does not exist in the database. Try to get them from LDAP.
// If user does not exist and authenticates successfully with LDAP we
// will create it on the fly and sign in with default permissions
if (!$user) {
LOG::debug("Local user ".Input::get('username')." does not exist");
LOG::debug("Creating local user ".Input::get('username'));
Log::debug("Local user ".Input::get('username')." does not exist");
Log::debug("Creating local user ".Input::get('username'));
if ($user = Ldap::createUserFromLdap($ldap_user)) { //this handles passwords on its own
LOG::debug("Local user created.");
Log::debug("Local user created.");
} else {
LOG::debug("Could not create local user.");
Log::debug("Could not create local user.");
throw new \Exception("Could not create local user");
}
// If the user exists and they were imported from LDAP already
} else {
LOG::debug("Local user ".$request->input('username')." exists in database. Updating existing user against LDAP.");
Log::debug("Local user ".$request->input('username')." exists in database. Updating existing user against LDAP.");
$ldap_attr = Ldap::parseAndMapLdapAttributes($ldap_user);
@@ -155,7 +161,7 @@ class LoginController extends Controller
// Should we even check for LDAP users?
if (Setting::getSettings()->ldap_enabled=='1') {
LOG::debug("LDAP is enabled.");
Log::debug("LDAP is enabled.");
try {
$user = $this->loginViaLdap($request);
Auth::login($user, true);
@@ -163,21 +169,21 @@ class LoginController extends Controller
// If the user was unable to login via LDAP, log the error and let them fall through to
// local authentication.
} catch (\Exception $e) {
LOG::error("There was an error authenticating the LDAP user: ".$e->getMessage());
Log::debug("There was an error authenticating the LDAP user: ".$e->getMessage());
}
}
// If the user wasn't authenticated via LDAP, skip to local auth
if (!$user) {
LOG::debug("Authenticating user against database.");
Log::debug("Authenticating user against database.");
// Try to log the user in
if (!Auth::attempt(Input::only('username', 'password'), Input::get('remember-me', 0))) {
if (!Auth::attempt(['username' => $request->input('username'), 'password' => $request->input('password'), 'activated' => 1], $request->input('remember'))) {
if (!$lockedOut) {
$this->incrementLoginAttempts($request);
}
LOG::debug("Local authentication failed.");
Log::debug("Local authentication failed.");
return redirect()->back()->withInput()->with('error', trans('auth/message.account_not_found'));
} else {
@@ -203,26 +209,33 @@ class LoginController extends Controller
public function getTwoFactorEnroll()
{
// Make sure the user is logged in
if (!Auth::check()) {
return redirect()->route('login')->with('error', 'You must be logged in.');
return redirect()->route('login')->with('error', trans('auth/general.login_prompt'));
}
$settings = Setting::getSettings();
$user = Auth::user();
$google2fa = app()->make('PragmaRX\Google2FA\Contracts\Google2FA');
if ($user->two_factor_secret=='') {
$user->two_factor_secret = $google2fa->generateSecretKey(32);
$user->save();
// We wouldn't normally see this page if 2FA isn't enforced via the
// \App\Http\Middleware\CheckForTwoFactor middleware AND if a device isn't enrolled,
// but let's check check anyway in case there's a browser history or back button thing.
// While you can access this page directly, enrolling a device when 2FA isn't enforced
// won't cause any harm.
if (($user->two_factor_secret!='') && ($user->two_factor_enrolled==1)) {
return redirect()->route('two-factor')->with('error', trans('auth/message.two_factor.already_enrolled'));
}
$google2fa = new Google2FA();
$secret = $google2fa->generateSecretKey();
$user->two_factor_secret = $secret;
$user->save();
$google2fa_url = $google2fa->getQRCodeGoogleUrl(
urlencode(Setting::getSettings()->site_name),
urlencode($user->username),
$user->two_factor_secret
);
return view('auth.two_factor_enroll')->with('google2fa_url', $google2fa_url);
$barcode = new \Com\Tecnick\Barcode\Barcode();
$barcode_obj = $barcode->getBarcodeObj('QRCODE', 'otpauth://totp/'.urlencode($settings->site_name).':'.urlencode($user->username).'?secret='.urlencode($secret).'&issuer=Snipe-IT&period=30', 300, 300, 'black', array(-2, -2, -2, -2));
return view('auth.two_factor_enroll')->with('barcode_obj', $barcode_obj);
}
@@ -234,6 +247,20 @@ class LoginController extends Controller
*/
public function getTwoFactorAuth()
{
// Check that the user is logged in
if (!Auth::check()) {
return redirect()->route('login')->with('error', trans('auth/general.login_prompt'));
}
$user = Auth::user();
// Check whether there is a device enrolled.
// This *should* be handled via the \App\Http\Middleware\CheckForTwoFactor middleware
// but we're just making sure (in case someone edited the database directly, etc)
if (($user->two_factor_secret=='') || ($user->two_factor_enrolled!=1)) {
return redirect()->route('two-factor-enroll');
}
return view('auth.two_factor');
}
@@ -246,22 +273,25 @@ class LoginController extends Controller
{
if (!Auth::check()) {
return redirect()->route('login')->with('error', 'You must be logged in.');
return redirect()->route('login')->with('error', trans('auth/general.login_prompt'));
}
if (!$request->has('two_factor_secret')) {
return redirect()->route('two-factor')->with('error', trans('auth/message.two_factor.code_required'));
}
$user = Auth::user();
$secret = $request->get('two_factor_secret');
$google2fa = app()->make('PragmaRX\Google2FA\Contracts\Google2FA');
$valid = $google2fa->verifyKey($user->two_factor_secret, $secret);
$google2fa = new Google2FA();
$secret = $request->input('two_factor_secret');
if ($valid) {
if ($google2fa->verifyKey($user->two_factor_secret, $secret)) {
$user->two_factor_enrolled = 1;
$user->save();
$request->session()->put('2fa_authed', 'true');
return redirect()->route('home')->with('success', 'You are logged in!');
}
return redirect()->route('two-factor')->with('error', 'Invalid two-factor code');
return redirect()->route('two-factor')->with('error', trans('auth/message.two_factor.invalid_code'));
}
@@ -284,7 +314,7 @@ class LoginController extends Controller
return redirect()->away($customLogoutUrl);
}
return redirect()->route('login')->with('success', 'You have successfully logged out!');
return redirect()->route('login')->with('success', trans('auth/general.logout.success'));
}
@@ -309,11 +339,11 @@ class LoginController extends Controller
}
/**
* Redirect the user after determining they are locked out.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
* Redirect the user after determining they are locked out.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
protected function sendLockoutResponse(Request $request)
{
$seconds = $this->limiter()->availableIn(
@@ -324,18 +354,18 @@ class LoginController extends Controller
$message = \Lang::get('auth/message.throttle', ['minutes' => $minutes]);
return redirect()->back()
return redirect()->back()
->withInput($request->only($this->username(), 'remember'))
->withErrors([$this->username() => $message]);
}
/**
* Override the lockout time and duration
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
* Override the lockout time and duration
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function hasTooManyLoginAttempts(Request $request)
{
$lockoutTime = config('auth.throttle.lockout_duration');

View File

@@ -4,6 +4,8 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use App\Models\User;
use Illuminate\Http\Request;
class ResetPasswordController extends Controller
{
@@ -36,4 +38,8 @@ class ResetPasswordController extends Controller
{
$this->middleware('guest');
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Http\Controllers\CheckInOutRequest;
use App\Models\Asset;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class BulkAssetsController extends Controller
{
use CheckInOutRequest;
/**
* Display the bulk edit page.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @return View
* @internal param int $assetId
* @since [v2.0]
*/
public function edit(Request $request)
{
$this->authorize('update', Asset::class);
if (!$request->has('ids')) {
return redirect()->back()->with('error', 'No assets selected');
}
$asset_ids = array_keys($request->input('ids'));
if ($request->has('bulk_actions')) {
switch($request->input('bulk_actions')) {
case 'labels':
return view('hardware/labels')
->with('assets', Asset::find($asset_ids))
->with('settings', Setting::getSettings())
->with('count', 0);
case 'delete':
$assets = Asset::with('assignedTo', 'location')->find($asset_ids);
$assets->each(function ($asset) {
$this->authorize('delete', $asset);
});
return view('hardware/bulk-delete')->with('assets', $assets);
case 'edit':
return view('hardware/bulk')
->with('assets', request('ids'))
->with('statuslabel_list', Helper::statusLabelList());
}
}
return redirect()->back()->with('error', 'No action selected');
}
/**
* Save bulk edits
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @return Redirect
* @internal param array $assets
* @since [v2.0]
*/
public function update(Request $request)
{
$this->authorize('update', Asset::class);
\Log::debug($request->input('ids'));
if(!$request->has('ids') || count($request->input('ids')) <= 0) {
return redirect()->route("hardware.index")->with('warning', trans('No assets selected, so nothing was updated.'));
}
$assets = array_keys($request->input('ids'));
if (($request->has('purchase_date'))
|| ($request->has('purchase_cost'))
|| ($request->has('supplier_id'))
|| ($request->has('order_number'))
|| ($request->has('warranty_months'))
|| ($request->has('rtd_location_id'))
|| ($request->has('requestable'))
|| ($request->has('company_id'))
|| ($request->has('status_id'))
|| ($request->has('model_id'))
) {
foreach ($assets as $assetId) {
$this->update_array = [];
$this->conditionallyAddItem('purchase_date')
->conditionallyAddItem('model_id')
->conditionallyAddItem('order_number')
->conditionallyAddItem('requestable')
->conditionallyAddItem('status_id')
->conditionallyAddItem('supplier_id')
->conditionallyAddItem('warranty_months');
if ($request->has('purchase_cost')) {
$this->update_array['purchase_cost'] = Helper::ParseFloat($request->input('purchase_cost'));
}
if ($request->has('company_id')) {
$this->update_array['company_id'] = $request->input('company_id');
if ($request->input('company_id')=="clear") {
$this->update_array['company_id'] = null;
}
}
if ($request->has('rtd_location_id')) {
$this->update_array['rtd_location_id'] = $request->input('rtd_location_id');
if (($request->has('update_real_loc')) && (($request->input('update_real_loc')) == '1')) {
$this->update_array['location_id'] = $request->input('rtd_location_id');
}
}
DB::table('assets')
->where('id', $assetId)
->update($this->update_array);
} // endforeach
return redirect()->route("hardware.index")->with('success', trans('admin/hardware/message.update.success'));
// no values given, nothing to update
}
return redirect()->route("hardware.index")->with('warning', trans('admin/hardware/message.update.nothing_updated'));
}
/**
* Array to store update data per item
* @var Array
*/
private $update_array;
/**
* Adds parameter to update array for an item if it exists in request
* @param String $field field name
* @return this Model for Chaining
*/
protected function conditionallyAddItem($field)
{
if(request()->has($field)) {
$this->update_array[$field] = request()->input($field);
}
return $this;
}
/**
* Save bulk deleted.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @return View
* @internal param array $assets
* @since [v2.0]
*/
public function destroy(Request $request)
{
$this->authorize('delete', Asset::class);
if ($request->has('ids')) {
$assets = Asset::find($request->get('ids'));
foreach ($assets as $asset) {
$update_array['deleted_at'] = date('Y-m-d H:i:s');
$update_array['assigned_to'] = null;
DB::table('assets')
->where('id', $asset->id)
->update($update_array);
} // endforeach
return redirect()->to("hardware")->with('success', trans('admin/hardware/message.delete.success'));
// no values given, nothing to update
}
return redirect()->to("hardware")->with('info', trans('admin/hardware/message.delete.nothing_updated'));
}
/**
* Show Bulk Checkout Page
* @return View View to checkout multiple assets
*/
public function showCheckout()
{
$this->authorize('checkout', Asset::class);
// Filter out assets that are not deployable.
return view('hardware/bulk-checkout');
}
/**
* Process Multiple Checkout Request
* @return View
*/
public function storeCheckout(Request $request)
{
try {
$admin = Auth::user();
$target = $this->determineCheckoutTarget();
if (!is_array($request->get('selected_assets'))) {
return redirect()->route('hardware/bulkcheckout')->withInput()->with('error', trans('admin/hardware/message.checkout.no_assets_selected'));
}
$asset_ids = array_filter($request->get('selected_assets'));
foreach ($asset_ids as $asset_id) {
if ($target->id == $asset_id && request('checkout_to_type') =='asset') {
return redirect()->back()->with('error', 'You cannot check an asset out to itself.');
}
}
$checkout_at = date("Y-m-d H:i:s");
if (($request->has('checkout_at')) && ($request->get('checkout_at')!= date("Y-m-d"))) {
$checkout_at = e($request->get('checkout_at'));
}
$expected_checkin = '';
if ($request->has('expected_checkin')) {
$expected_checkin = e($request->get('expected_checkin'));
}
$errors = [];
DB::transaction(function () use ($target, $admin, $checkout_at, $expected_checkin, $errors, $asset_ids, $request) {
foreach ($asset_ids as $asset_id) {
$asset = Asset::findOrFail($asset_id);
$this->authorize('checkout', $asset);
$error = $asset->checkOut($target, $admin, $checkout_at, $expected_checkin, e($request->get('note')), null);
if ($target->location_id!='') {
$asset->location_id = $target->location_id;
$asset->unsetEventDispatcher();
$asset->save();
}
if ($error) {
array_merge_recursive($errors, $asset->getErrors()->toArray());
}
}
});
if (!$errors) {
// Redirect to the new asset page
return redirect()->to("hardware")->with('success', trans('admin/hardware/message.checkout.success'));
}
// Redirect to the asset management page with error
return redirect()->to("hardware/bulk-checkout")->with('error', trans('admin/hardware/message.checkout.error'))->withErrors($errors);
} catch (ModelNotFoundException $e) {
return redirect()->to("hardware/bulk-checkout")->with('error', $e->getErrors());
}
}
}

View File

@@ -179,7 +179,7 @@ class CategoriesController extends Controller
try {
unlink(app('categories_upload_path').$old_image);
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
use App\Exceptions\CheckoutNotAllowed;
use App\Models\Asset;
use App\Models\Location;
use App\Models\User;
trait CheckInOutRequest
{
/**
* Find target for checkout
* @return SnipeModel Target asset is being checked out to.
*/
protected function determineCheckoutTarget()
{
// This item is checked out to a location
switch(request('checkout_to_type'))
{
case 'location':
return Location::findOrFail(request('assigned_location'));
case 'asset':
return Asset::findOrFail(request('assigned_asset'));
case 'user':
return User::findOrFail(request('assigned_user'));
}
return null;
}
/**
* Update the location of the asset passed in.
* @param Asset $asset Asset being updated
* @param SnipeModel $target Target with location
* @return Asset Asset being updated
*/
protected function updateAssetLocation($asset, $target)
{
switch(request('checkout_to_type'))
{
case 'location':
$asset->location_id = $target->id;
break;
case 'asset':
$asset->location_id = $target->rtd_location_id;
// Override with the asset's location_id if it has one
if ($target->location_id!='') {
$asset->location_id = $target->location_id;
}
break;
case 'user':
$asset->location_id = $target->location_id;
break;
}
return $asset;
}
}

View File

@@ -29,7 +29,9 @@ final class CompaniesController extends Controller
*/
public function index()
{
return view('companies/index')->with('companies', Company::all());
$this->authorize('view', Company::class);
return view('companies/index');
}
/**
@@ -41,6 +43,8 @@ final class CompaniesController extends Controller
*/
public function create()
{
$this->authorize('create', Company::class);
return view('companies/edit')->with('item', new Company);
}
@@ -54,6 +58,8 @@ final class CompaniesController extends Controller
*/
public function store(ImageUploadRequest $request)
{
$this->authorize('create', Company::class);
$company = new Company;
$company->name = $request->input('name');
@@ -90,6 +96,9 @@ final class CompaniesController extends Controller
return redirect()->route('companies.index')
->with('error', trans('admin/companies/message.does_not_exist'));
}
$this->authorize('update', $item);
return view('companies/edit')->with('item', $item);
}
@@ -108,6 +117,8 @@ final class CompaniesController extends Controller
return redirect()->route('companies.index')->with('error', trans('admin/companies/message.does_not_exist'));
}
$this->authorize('update', $company);
$company->name = $request->input('name');
$old_image = $company->image;
@@ -137,7 +148,7 @@ final class CompaniesController extends Controller
try {
unlink(app('companies_upload_path').$old_image);
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}
@@ -164,6 +175,9 @@ final class CompaniesController extends Controller
return redirect()->route('companies.index')
->with('error', trans('admin/companies/message.not_found'));
} else {
$this->authorize('delete', $company);
try {
$company->delete();
return redirect()->route('companies.index')

View File

@@ -202,19 +202,6 @@ class ComponentsController extends Controller
return redirect()->route('components.index')->with('success', trans('admin/components/message.delete.success'));
}
public function postBulk($componentId = null)
{
//$this->authorize('checkout', $component)
echo 'Stubbed - not yet complete';
}
public function postBulkSave($componentId = null)
{
//$this->authorize('edit', Component::class);
echo 'Stubbed - not yet complete';
}
/**
* Return a view to display component information.
*

View File

@@ -37,6 +37,7 @@ class CustomFieldsController extends Controller
*/
public function index()
{
$this->authorize('view', CustomField::class);
$fieldsets = CustomFieldset::with("fields", "models")->get();
$fields = CustomField::with("fieldset")->get();
@@ -57,6 +58,7 @@ class CustomFieldsController extends Controller
*/
public function create()
{
$this->authorize('create', CustomField::class);
return view("custom_fields.fields.edit")->with('field', new CustomField());
}
@@ -72,6 +74,8 @@ class CustomFieldsController extends Controller
*/
public function store(CustomFieldRequest $request)
{
$this->authorize('create', CustomField::class);
$field = new CustomField([
"name" => $request->get("name"),
"element" => $request->get("element"),
@@ -83,7 +87,7 @@ class CustomFieldsController extends Controller
]);
if (!in_array(Input::get('format'), array_keys(CustomField::$PredefinedFormats))) {
if ($request->has("custom_format")) {
$field->format = e($request->get("custom_format"));
} else {
$field->format = e($request->get("format"));
@@ -92,7 +96,6 @@ class CustomFieldsController extends Controller
if ($field->save()) {
return redirect()->route("fields.index")->with("success", trans('admin/custom_fields/message.field.create.success'));
} else {
// dd($field);
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.field.create.error'));
}
@@ -110,6 +113,8 @@ class CustomFieldsController extends Controller
{
$field = CustomField::find($field_id);
$this->authorize('update', $field);
if ($field->fieldset()->detach($fieldset_id)) {
return redirect()->route('fieldsets.show', ['fieldset' => $fieldset_id])->with("success", trans('admin/custom_fields/message.field.delete.success'));
}
@@ -128,6 +133,8 @@ class CustomFieldsController extends Controller
{
$field = CustomField::find($field_id);
$this->authorize('delete', $field);
if ($field->fieldset->count()>0) {
return redirect()->back()->withErrors(['message' => "Field is in-use"]);
} else {
@@ -149,6 +156,9 @@ class CustomFieldsController extends Controller
public function edit($id)
{
$field = CustomField::find($id);
$this->authorize('update', $field);
return view("custom_fields.fields.edit")->with('field', $field);
}
@@ -166,10 +176,12 @@ class CustomFieldsController extends Controller
public function update(CustomFieldRequest $request, $id)
{
$field = CustomField::find($id);
$this->authorize('update', $field);
$field->name = e($request->get("name"));
$field->element = e($request->get("element"));
$field->field_values = e($request->get("field_values"));
$field->field_encrypted = e($request->get("field_encrypted", 0));
$field->user_id = Auth::user()->id;
$field->help_text = $request->get("help_text");
$field->show_in_email = $request->get("show_in_email", 0);

View File

@@ -38,6 +38,8 @@ class CustomFieldsetsController extends Controller
{
$cfset = CustomFieldset::with('fields')->where('id', '=', $id)->orderBy('id', 'ASC')->first();
$this->authorize('view', $cfset);
if ($cfset) {
$custom_fields_list = ["" => "Add New Field to Fieldset"] + CustomField::pluck("name", "id")->toArray();
@@ -68,6 +70,8 @@ class CustomFieldsetsController extends Controller
*/
public function create()
{
$this->authorize('create', CustomFieldset::class);
return view("custom_fields.fieldsets.edit");
}
@@ -81,6 +85,8 @@ class CustomFieldsetsController extends Controller
*/
public function store(Request $request)
{
$this->authorize('create', CustomFieldset::class);
$cfset = new CustomFieldset(
[
"name" => e($request->get("name")),
@@ -141,6 +147,8 @@ class CustomFieldsetsController extends Controller
{
$fieldset = CustomFieldset::find($id);
$this->authorize('delete', $fieldset);
if ($fieldset) {
$models = AssetModel::where("fieldset_id", "=", $id);
if ($models->count() == 0) {
@@ -169,6 +177,8 @@ class CustomFieldsetsController extends Controller
$set = CustomFieldset::find($id);
$this->authorize('update', $set);
foreach ($set->fields as $field) {
if ($field->id == Input::get('field_id')) {
return redirect()->route("fieldsets.show", [$id])->withInput()->withErrors(['field_id' => trans('admin/custom_fields/message.field.already_added')]);

View File

@@ -83,6 +83,8 @@ class DepartmentsController extends Controller
{
$department = Department::find($id);
$this->authorize('view', $department);
if (isset($department->id)) {
return view('departments/view', compact('department'));
}
@@ -100,6 +102,8 @@ class DepartmentsController extends Controller
*/
public function create()
{
$this->authorize('create', Department::class);
return view('departments/edit')->with('item', new Department);
}
@@ -118,6 +122,8 @@ class DepartmentsController extends Controller
return redirect()->to(route('departments.index'))->with('error', trans('admin/departments/message.not_found'));
}
$this->authorize('delete', $department);
if ($department->users->count() > 0) {
return redirect()->to(route('departments.index'))->with('error', trans('admin/departments/message.assoc_users'));
}
@@ -141,16 +147,20 @@ class DepartmentsController extends Controller
if (is_null($item = Department::find($id))) {
return redirect()->back()->with('error', trans('admin/locations/message.does_not_exist'));
}
$this->authorize('update', $item);
return view('departments/edit', compact('item'));
}
public function update(ImageUploadRequest $request, $id) {
$this->authorize('create', Department::class);
if (is_null($department = Department::find($id))) {
return redirect()->route('departments.index')->with('error', trans('admin/departments/message.does_not_exist'));
}
$this->authorize('update', $department);
$department->fill($request->all());
$department->manager_id = ($request->has('manager_id' ) ? $request->input('manager_id') : null);
@@ -181,7 +191,7 @@ class DepartmentsController extends Controller
try {
unlink(app('departments_upload_path').$old_image);
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}

View File

@@ -31,8 +31,10 @@ class DepreciationsController extends Controller
*/
public function index()
{
$this->authorize('view', Depreciation::class);
// Show the page
return view('depreciations/index', compact('depreciations'));
return view('depreciations/index');
}
@@ -46,6 +48,8 @@ class DepreciationsController extends Controller
*/
public function create()
{
$this->authorize('create', Depreciation::class);
// Show the page
return view('depreciations/edit')->with('item', new Depreciation);
}
@@ -62,6 +66,8 @@ class DepreciationsController extends Controller
*/
public function store(Request $request)
{
$this->authorize('create', Depreciation::class);
// create a new instance
$depreciation = new Depreciation();
// Depreciation data
@@ -94,6 +100,8 @@ class DepreciationsController extends Controller
return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist'));
}
$this->authorize('update', $item);
return view('depreciations/edit', compact('item'));
}
@@ -116,6 +124,8 @@ class DepreciationsController extends Controller
return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist'));
}
$this->authorize('update', $depreciation);
// Depreciation data
$depreciation->name = $request->input('name');
$depreciation->months = $request->input('months');
@@ -145,6 +155,8 @@ class DepreciationsController extends Controller
return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.not_found'));
}
$this->authorize('delete', $depreciation);
if ($depreciation->has_models() > 0) {
// Redirect to the asset management page
return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.assoc_users'));
@@ -171,6 +183,8 @@ class DepreciationsController extends Controller
return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist'));
}
$this->authorize('view', $depreciation);
return view('depreciations/view', compact('depreciation'));
}

View File

@@ -31,7 +31,7 @@ class GroupsController extends Controller
public function index()
{
// Show the page
return view('groups/index', compact('groups'));
return view('groups/index');
}
/**
@@ -72,7 +72,7 @@ class GroupsController extends Controller
if ($group->save()) {
return redirect()->route("groups.index")->with('success', trans('admin/groups/message.success.create'));
}
return redirect(route('groups.create'))->withInput()->withErrors($group->getErrors());
return redirect()->back()->withInput()->withErrors($group->getErrors());
}
/**
@@ -111,7 +111,7 @@ class GroupsController extends Controller
{
$permissions = config('permissions');
if (!$group = Group::find($id)) {
return redirect()->route('groups')->with('error', trans('admin/groups/message.group_not_found', compact('id')));
return redirect()->route('groups.index')->with('error', trans('admin/groups/message.group_not_found', compact('id')));
}
$group->name = e(Input::get('name'));
$group->permissions = json_encode(Input::get('permission'));
@@ -138,7 +138,7 @@ class GroupsController extends Controller
{
if (!config('app.lock_passwords')) {
if (!$group = Group::find($id)) {
return redirect()->route('groups')->with('error', trans('admin/groups/message.group_not_found', compact('id')));
return redirect()->route('groups.index')->with('error', trans('admin/groups/message.group_not_found', compact('id')));
}
$group->delete();
// Redirect to the group management page

View File

@@ -12,7 +12,7 @@ class ImportsController extends Controller
{
public function index()
{
$this->authorize('create', Asset::class);
$this->authorize('import');
$imports = Import::latest()->get();
$imports = (new ImportsTransformer)->transformImports($imports);
return view('importer/import')->with('imports', $imports);

View File

@@ -1,6 +1,7 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\AssetFileRequest;
use Assets;
use Illuminate\Support\Facades\Session;
use Input;
@@ -178,7 +179,9 @@ class LicensesController extends Controller
$license->purchase_date = $request->input('purchase_date');
$license->purchase_order = $request->input('purchase_order');
$license->reassignable = $request->input('reassignable', 0);
$license->serial = $request->input('serial');
if (Gate::allows('viewKeys', $license)) {
$license->serial = $request->input('serial');
}
$license->termination_date = $request->input('termination_date');
$license->seats = e($request->input('seats'));
$license->manufacturer_id = $request->input('manufacturer_id');
@@ -264,31 +267,40 @@ class LicensesController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param Request $request
* @param int $licenseId
* @param int $seatId
* @return \Illuminate\Http\RedirectResponse
*/
public function postCheckout(Request $request, $licenseId)
public function postCheckout(Request $request, $licenseId, $seatId = null)
{
// Check that the license is valid
if ($license = License::where('id',$licenseId)->first()) {
if ($license = License::where('id', $licenseId)->first()) {
// If the license is valid, check that there is an available seat
if ($license->getAvailSeatsCountAttribute() < 1) {
return redirect()->route('licenses.index')->with('error', 'There are no available seats for this license');
}
// Get the next available seat for this license
$next = $license->freeSeat();
if (!$next) {
return redirect()->route('licenses.index')->with('error', 'There are no available seats for this license');
if (!$seatId) {
// Get the next available seat for this license
$next = $license->freeSeat();
if (!$next) {
return redirect()->route('licenses.index')->with('error', 'There are no available seats for this license');
}
if (!$licenseSeat = LicenseSeat::where('id', '=', $next->id)->first()) {
return redirect()->route('licenses.index')->with('error', 'There are no available seats for this license');
}
} else {
$licenseSeat = LicenseSeat::where('id', '=', $seatId)->first();
if (!$licenseSeat) {
return redirect()->route('licenses.index')->with('error', 'License seat is not available for checkout');
}
}
if (!$licenseSeat = LicenseSeat::where('id', '=', $next->id)->first()) {
return redirect()->route('licenses.index')->with('error', 'There are no available seats for this license');
}
$this->authorize('checkout', $license);
@@ -403,20 +415,7 @@ class LicensesController extends Controller
return redirect()->back()->withInput();
}
// Declare the rules for the form validation
$rules = array(
'note' => 'string',
'notes' => 'string',
);
// Create a new validator instance from our validation rules
$validator = Validator::make(Input::all(), $rules);
// If validation fails, we'll exit the operation now.
if ($validator->fails()) {
// Ooops.. something went wrong
return redirect()->back()->withInput()->withErrors($validator);
}
$return_to = User::find($licenseSeat->assigned_to);
if (!$return_to) {
$return_to = Asset::find($licenseSeat->asset_id);
@@ -428,7 +427,7 @@ class LicensesController extends Controller
// Was the asset updated?
if ($licenseSeat->save()) {
$licenseSeat->logCheckin($return_to, e(request('note')));
$licenseSeat->logCheckin($license, e(request('note')));
if ($backTo=='user') {
return redirect()->route("users.show", $return_to->id)->with('success', trans('admin/licenses/message.checkin.success'));
}
@@ -495,7 +494,7 @@ class LicensesController extends Controller
* @param int $licenseId
* @return \Illuminate\Http\RedirectResponse
*/
public function postUpload(Request $request, $licenseId = null)
public function postUpload(AssetFileRequest $request, $licenseId = null)
{
$license = License::find($licenseId);
// the license is valid
@@ -504,18 +503,9 @@ class LicensesController extends Controller
if (isset($license->id)) {
$this->authorize('update', $license);
if (Input::hasFile('licensefile')) {
if (Input::hasFile('file')) {
foreach (Input::file('licensefile') as $file) {
$rules = array(
'licensefile' => 'required|mimes:png,gif,jpg,jpeg,doc,docx,pdf,txt,zip,rar,rtf,xml,lic'
);
$validator = Validator::make(array('licensefile'=> $file), $rules);
if ($validator->fails()) {
return redirect()->back()->with('error', trans('admin/licenses/message.upload.invalidfiles'));
}
foreach (Input::file('file') as $file) {
$extension = $file->getClientOriginalExtension();
$filename = 'license-'.$license->id.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$extension;
$upload_success = $file->move($destinationPath, $filename);

View File

@@ -44,7 +44,7 @@ class LocationsController extends Controller
$locations = Location::orderBy('created_at', 'DESC')->with('parent', 'assets', 'assignedassets')->get();
// Show the page
return view('locations/index', compact('locations'));
return view('locations/index');
}
@@ -114,42 +114,6 @@ class LocationsController extends Controller
return redirect()->back()->withInput()->withErrors($location->getErrors());
}
/**
* Validates and stores a new location created via the Create Asset form modal.
*
* @todo Check if a Form Request would work better here.
* @author [A. Gianotto] [<snipe@snipe.net>]
* @see AssetsController::getCreate() method that makes the form
* @since [v1.0]
* @return String JSON
*/
public function apiStore(Request $request)
{
$this->authorize('create', Location::class);
$new['currency']=Setting::first()->default_currency;
// create a new location instance
$location = new Location();
// Save the location data
$location->name = $request->input('name');
$location->currency = Setting::first()->default_currency; //e(Input::get('currency'));
$location->address = ''; //e(Input::get('address'));
// $location->address2 = e(Input::get('address2'));
$location->city = $request->input('city');
$location->state = '';//e(Input::get('state'));
$location->country = $request->input('country');
// $location->zip = e(Input::get('zip'));
$location->user_id = Auth::id();
// Was the location created?
if ($location->save()) {
return JsonResponse::create($location);
}
// failure
return JsonResponse::create(["error" => "Failed validation: ".print_r($location->getErrors(), true)], 500);
}
/**
* Makes a form view to edit location information.
@@ -162,7 +126,7 @@ class LocationsController extends Controller
*/
public function edit($locationId = null)
{
$this->authorize('edit', Location::class);
$this->authorize('update', Location::class);
// Check if the location exists
if (is_null($item = Location::find($locationId))) {
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
@@ -175,8 +139,7 @@ class LocationsController extends Controller
$location_options = array('' => 'Top Level') + $location_options;
return view('locations/edit', compact('item'))
->with('location_options', $location_options)
->with('manager_list', Helper::managerList());
->with('location_options', $location_options);
}
@@ -191,7 +154,7 @@ class LocationsController extends Controller
*/
public function update(ImageUploadRequest $request, $locationId = null)
{
$this->authorize('edit', Location::class);
$this->authorize('update', Location::class);
// Check if the location exists
if (is_null($location = Location::find($locationId))) {
return redirect()->route('locations.index')->with('error', trans('admin/locations/message.does_not_exist'));
@@ -237,7 +200,7 @@ class LocationsController extends Controller
try {
unlink(app('locations_upload_path').$old_image);
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}

View File

@@ -36,7 +36,7 @@ class ManufacturersController extends Controller
public function index()
{
$this->authorize('index', Manufacturer::class);
return view('manufacturers/index', compact('manufacturers'));
return view('manufacturers/index');
}
@@ -170,7 +170,7 @@ class ManufacturersController extends Controller
try {
unlink(app('manufacturers_upload_path').$old_image);
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}
@@ -207,7 +207,7 @@ class ManufacturersController extends Controller
try {
unlink(public_path().'/uploads/manufacturers/'.$manufacturer->image);
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}

View File

@@ -13,9 +13,7 @@ class ModalController extends Controller
}
function model() {
return view('modals.model')
->with('manufacturer', Helper::manufacturerList())
->with('category', Helper::categoryList('asset'));
return view('modals.model');
}
function statuslabel() {

View File

@@ -45,15 +45,24 @@ class ProfileController extends Controller
{
$user = Auth::user();
$user->first_name = Input::get('first_name');
$user->last_name = Input::get('last_name');
$user->website = Input::get('website');
$user->location_id = Input::get('location_id');
$user->gravatar = Input::get('gravatar');
$user->locale = Input::get('locale');
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->website = $request->input('website');
$user->gravatar = $request->input('gravatar');
$user->phone = $request->input('phone');
if (!config('app.lock_passwords')) {
$user->locale = $request->input('locale', 'en');
}
if ((Gate::allows('self.two_factor')) && ((Setting::getSettings()->two_factor_enabled=='1') && (!config('app.lock_passwords')))) {
$user->two_factor_optin = Input::get('two_factor_optin', '0');
$user->two_factor_optin = $request->input('two_factor_optin', '0');
}
if (Gate::allows('self.edit_location') && (!config('app.lock_passwords'))) {
$user->location_id = $request->input('location_id');
}
if (Input::file('avatar')) {
@@ -119,8 +128,7 @@ class ProfileController extends Controller
$rules = array(
'current_password' => 'required',
'password' => Setting::passwordComplexityRulesSaving('store'),
'password_confirm' => 'required|same:password',
'password' => Setting::passwordComplexityRulesSaving('store').'|confirmed',
);
$validator = \Validator::make($request->all(), $rules);

View File

@@ -26,6 +26,14 @@ use Illuminate\Http\Request;
*/
class ReportsController extends Controller
{
/**
* Checks for correct permissions
*/
public function __construct() {
parent::__construct();
$this->authorize('reports.view');
}
/**
* Returns a view that displays the accessories report.
@@ -595,8 +603,8 @@ class ReportsController extends Controller
if ($request->has('assigned_to')) {
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? e($asset->assigned->getFullNameAttribute()) : ($asset->assigned ? e($asset->assigned->display_name) : '');
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? 'user' : e($asset->assignedType());
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? $asset->assigned->getFullNameAttribute() : ($asset->assigned ? $asset->assigned->display_name : '');
$row[] = ($asset->checkedOutToUser() && $asset->assigned) ? 'user' : $asset->assignedType();
}
if ($request->has('username')) {

View File

@@ -401,6 +401,8 @@ class SettingsController extends Controller
$setting->footer_text = $request->input('footer_text');
$setting->skin = $request->input('skin');
$setting->show_url_in_emails = $request->input('show_url_in_emails', '0');
$setting->logo_print_assets = $request->input('logo_print_assets', '0');
// Only allow the site name and CSS to be changed if lock_passwords is false
@@ -480,13 +482,12 @@ class SettingsController extends Controller
$setting->two_factor_enabled = null;
} else {
$setting->two_factor_enabled = $request->input('two_factor_enabled');
# remote user login
$setting->login_remote_user_enabled = (int)$request->input('login_remote_user_enabled');
$setting->login_common_disabled= (int)$request->input('login_common_disabled');
$setting->login_remote_user_custom_logout_url = $request->input('login_remote_user_custom_logout_url');
}
# remote user login
$setting->login_remote_user_enabled = (int)$request->input('login_remote_user_enabled');
$setting->login_common_disabled = (int)$request->input('login_common_disabled');
$setting->login_remote_user_custom_logout_url = $request->input('login_remote_user_custom_logout_url');
}
$setting->pwd_secure_uncommon = (int) $request->input('pwd_secure_uncommon');
@@ -537,7 +538,9 @@ class SettingsController extends Controller
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
}
$setting->locale = $request->input('locale', 'en');
if (!config('app.lock_passwords')) {
$setting->locale = $request->input('locale', 'en');
}
$setting->default_currency = $request->input('default_currency', '$');
$setting->date_display_format = $request->input('date_display_format');
$setting->time_display_format = $request->input('time_display_format');
@@ -791,22 +794,34 @@ class SettingsController extends Controller
if (Input::has('labels_display_name')) {
if ($request->has('labels_display_name')) {
$setting->labels_display_name = 1;
} else {
$setting->labels_display_name = 0;
}
if (Input::has('labels_display_serial')) {
if ($request->has('labels_display_serial')) {
$setting->labels_display_serial = 1;
} else {
$setting->labels_display_serial = 0;
}
if (Input::has('labels_display_tag')) {
if ($request->has('labels_display_tag')) {
$setting->labels_display_tag = 1;
} else {
$setting->labels_display_tag = 0;
}
if ($request->has('labels_display_tag')) {
$setting->labels_display_tag = 1;
} else {
$setting->labels_display_tag = 0;
}
if ($request->has('labels_display_model')) {
$setting->labels_display_model = 1;
} else {
$setting->labels_display_model = 0;
}
if ($setting->save()) {

View File

@@ -33,7 +33,7 @@ class StatuslabelsController extends Controller
public function index()
{
$this->authorize('view', Statuslabel::class);
return view('statuslabels.index', compact('statuslabels'));
return view('statuslabels.index');
}
public function show($id)
@@ -103,36 +103,6 @@ class StatuslabelsController extends Controller
return redirect()->back()->withInput()->withErrors($statusLabel->getErrors());
}
/**
* @param Request $request
* @return JsonResponse
*/
public function apiStore(Request $request)
{
$this->authorize('create', Statuslabel::class);
$statuslabel = new Statuslabel();
if (!$request->has('statuslabel_types')) {
return JsonResponse::create(["error" => trans('validation.statuslabel_type')], 500);
}
$statustype = Statuslabel::getStatuslabelTypesForDB(Input::get('statuslabel_types'));
$statuslabel->name = Input::get('name');
$statuslabel->user_id = Auth::id();
$statuslabel->notes = '';
$statuslabel->deployable = $statustype['deployable'];
$statuslabel->pending = $statustype['pending'];
$statuslabel->archived = $statustype['archived'];
if ($statuslabel->isValid()) {
$statuslabel->save();
// Redirect to the new Statuslabel page
return JsonResponse::create($statuslabel);
}
return JsonResponse::create(["error" => $statuslabel->getErrors()->first()], 500);
}
/**
* Statuslabel update.
*

View File

@@ -34,10 +34,9 @@ class SuppliersController extends Controller
{
// Grab all the suppliers
$this->authorize('view', Supplier::class);
$suppliers = Supplier::orderBy('created_at', 'DESC')->get();
// Show the page
return view('suppliers/index', compact('suppliers'));
return view('suppliers/index');
}
@@ -97,23 +96,6 @@ class SuppliersController extends Controller
return redirect()->back()->withInput()->withErrors($supplier->getErrors());
}
/**
* @param Request $request
* @return JsonResponse
*/
public function apiStore(Request $request)
{
$this->authorize('create', Supplier::class);
$supplier = new Supplier;
$supplier->name = $request->input('name');
$supplier->user_id = Auth::id();
if ($supplier->save()) {
return JsonResponse::create($supplier);
}
return JsonResponse::create(["error" => "Failed validation: ".print_r($supplier->getErrors(), true)], 500);
}
/**
* Supplier update.
*
@@ -192,7 +174,7 @@ class SuppliersController extends Controller
try {
unlink(app('suppliers_upload_path').$old_image);
} catch (\Exception $e) {
\Log::error($e);
\Log::info($e);
}
}

View File

@@ -1,26 +1,30 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\AssetFileRequest;
use App\Helpers\Helper;
use App\Http\Requests\AssetFileRequest;
use App\Http\Requests\SaveUserRequest;
use App\Models\Accessory;
use App\Models\LicenseSeat;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Group;
use App\Models\Company;
use App\Models\Location;
use App\Models\License;
use App\Models\Setting;
use App\Http\Requests\SaveUserRequest;
use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Models\User;
use App\Models\Group;
use App\Models\Ldap;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\Location;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
use Artisan;
use Auth;
use Config;
use Crypt;
use DB;
use Gate;
use HTML;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Input;
use Lang;
use League\Csv\Reader;
@@ -29,12 +33,9 @@ use Redirect;
use Response;
use Str;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
use URL;
use View;
use Illuminate\Http\Request;
use Gate;
use Artisan;
use App\Notifications\WelcomeNotification;
/**
* This controller handles all actions related to Users for
@@ -85,8 +86,11 @@ class UsersController extends Controller
$userPermissions = Helper::selectedPermissionsArray($permissions, Input::old('permissions', array()));
$permissions = $this->filterDisplayable($permissions);
$user = new User;
$user->activated = 1;
return view('users/edit', compact('groups', 'userGroups', 'permissions', 'userPermissions'))
->with('user', new User);
->with('user', $user);
}
/**
@@ -111,7 +115,7 @@ class UsersController extends Controller
$user->last_name = $request->input('last_name');
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', $user->activated);
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle');
$user->phone = $request->input('phone');
$user->location_id = $request->input('location_id', null);
@@ -163,68 +167,7 @@ class UsersController extends Controller
return redirect()->back()->withInput()->withErrors($user->getErrors());
}
/**
* JSON handler for creating a user through a modal popup
*
* @todo Handle validation more graciously
* @author [B. Wetherington] [<uberbrady@gmail.com>]
* @since [v1.8]
* @return string JSON
*/
public function apiStore(SaveUserRequest $request)
{
$this->authorize('create', User::class);
$user = new User;
$inputs = Input::except('csrf_token', 'password_confirm', 'groups', 'email_user');
$inputs['activated'] = true;
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->username = $request->input('username');
$user->email = $request->input('email');
$user->department_id = $request->input('department_id', null);
if ($request->has('password')) {
$user->password = bcrypt($request->input('password'));
}
$user->activated = true;
// Was the user created?
if ($user->save()) {
if (Input::get('email_user') == 1) {
// Send the credentials through email
$data = array();
$data['email'] = $request->input('email');
$data['username'] = $request->input('username');
$data['first_name'] = $request->input('first_name');
$data['last_name'] = e($request->input('last_name'));
$data['password'] = $request->input('password');
$user->notify(new WelcomeNotification($data));
/*Mail::send('emails.send-login', $data, function ($m) use ($user) {
$m->to($user->email, $user->first_name . ' ' . $user->last_name);
$m->replyTo(config('mail.reply_to.address'), config('mail.reply_to.name'));
$m->subject(trans('mail.welcome', ['name' => $user->first_name]));
});*/
}
return JsonResponse::create($user);
}
return JsonResponse::create(["error" => "Failed validation: " . print_r($user->getErrors(), true)], 500);
}
/**
* Returns a view that displays the edit user form
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param $permissions
* @return View
* @internal param int $id
*/
private function filterDisplayable($permissions)
{
@@ -237,6 +180,15 @@ class UsersController extends Controller
return $output;
}
/**
* Returns a view that displays the edit user form
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param $permissions
* @return View
* @internal param int $id
*/
public function edit($id)
{
@@ -298,19 +250,16 @@ class UsersController extends Controller
}
}
} catch (UserNotFoundException $e) {
$error = trans('admin/users/message.user_not_found', compact('id'));
return redirect()->route('users.index')->with('error', $error);
} catch (ModelNotFoundException $e) {
return redirect()->route('users.index')
->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
// Only save groups if the user is a super user
if (Auth::user()->isSuperUser()) {
if ($request->has('groups')) {
$user->groups()->sync($request->input('groups'));
} else {
$user->groups()->sync(array());
}
$user->groups()->sync($request->input('groups'));
}
@@ -326,7 +275,7 @@ class UsersController extends Controller
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', $user->activated);
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone');
$user->location_id = $request->input('location_id', null);
@@ -338,6 +287,7 @@ class UsersController extends Controller
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
$user->activated = $request->input('activated', 0);
$user->zip = $request->input('zip', null);
@@ -382,7 +332,7 @@ class UsersController extends Controller
{
try {
// Get user information
$user = User::find($id);
$user = User::findOrFail($id);
// Authorize takes care of many of our logic checks now.
$this->authorize('delete', User::class);
@@ -420,7 +370,7 @@ class UsersController extends Controller
// Redirect to the user management page
return redirect()->route('users.index')->with('success', $success);
} catch (UserNotFoundException $e) {
} catch (ModelNotFoundException $e) {
// Prepare the error message
$error = trans('admin/users/message.user_not_found', compact('id'));
// Redirect to the user management page
@@ -551,7 +501,7 @@ class UsersController extends Controller
if (($key = array_search(Auth::user()->id, $user_raw_array)) !== false) {
unset($user_raw_array[$key]);
}
if (!config('app.lock_passwords')) {
@@ -762,129 +712,6 @@ class UsersController extends Controller
}
}
/**
* Return user import view
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @return \Illuminate\Contracts\View\View
*/
public function getImport()
{
$this->authorize('update', User::class);
// Selected groups
$selectedGroups = Input::old('groups', array());
// Get all the available permissions
$permissions = config('permissions');
$selectedPermissions = Input::old('permissions', array('superuser' => -1));
// Show the page
return view('users/import', compact('selectedGroups', 'permissions', 'selectedPermissions'));
}
/**
* Handle user import file
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @return \Illuminate\Http\RedirectResponse
*/
public function postImport()
{
$this->authorize('update', User::class);
if (!ini_get("auto_detect_line_endings")) {
ini_set("auto_detect_line_endings", '1');
}
$csv = Reader::createFromPath(Input::file('user_import_csv'));
$csv->setNewline("\r\n");
if (Input::get('has_headers') == 1) {
$csv->setOffset(1);
}
$duplicates = '';
$nbInsert = $csv->each(function ($row) use ($duplicates) {
if (array_key_exists(2, $row)) {
if (Input::get('activate') == 1) {
$activated = '1';
} else {
$activated = '0';
}
$pass = substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, 15);
// Location
if (array_key_exists('4', $row)) {
$user_location_id = trim($row[4]);
if ($user_location_id=='') {
$user_location_id = null;
}
}
try {
// Check if this email already exists in the system
$user = User::where('username', $row[2])->first();
if ($user) {
$duplicates .= $row[2] . ', ';
} else {
$newuser = array(
'first_name' => trim(e($row[0])),
'last_name' => trim(e($row[1])),
'username' => trim(e($row[2])),
'email' => trim(e($row[3])),
'password' => bcrypt($pass),
'activated' => $activated,
'location_id' => trim(e($user_location_id)),
'phone' => trim(e($row[5])),
'jobtitle' => trim(e($row[6])),
'employee_num' => trim(e($row[7])),
'company_id' => Company::getIdForUser($row[8]),
'permissions' => '{"user":1}',
'notes' => 'Imported user'
);
DB::table('users')->insert($newuser);
if (((Input::get('email_user') == 1) && !config('app.lock_passwords'))) {
// Send the credentials through email
if ($row[3] != '') {
$data = array();
$data['email'] = trim(e($row[4]));
$data['username'] = trim(e($row[2]));
$data['first_name'] = trim(e($row[0]));
$data['last_name'] = trim(e($row[1]));
$data['password'] = $pass;
if ($newuser['email']) {
$user = User::where('username', $row[2])->first();
$user->notify(new WelcomeNotification($data));
/*Mail::send('emails.send-login', $data, function ($m) use ($newuser) {
$m->to($newuser['email'], $newuser['first_name'] . ' ' . $newuser['last_name']);
$m->replyTo(config('mail.reply_to.address'), config('mail.reply_to.name'));
$m->subject(trans('mail.welcome', ['name' => $newuser['first_name']]));
});*/
}
}
}
}
} catch (Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "\n";
}
return true;
}
});
return redirect()->route('users.index')->with('duplicates', $duplicates)->with('success', 'Success');
}
/**
* Return JSON response with a list of user details for the getIndex() view.
*
@@ -1011,8 +838,7 @@ class UsersController extends Controller
return redirect()->route('users.index')->with('error', $e->getMessage());
}
return view('users/ldap')
->with('location_list', Helper::locationsList());
return view('users/ldap');
}
@@ -1156,7 +982,7 @@ class UsersController extends Controller
*/
public function printInventory($id)
{
$this->authorize('view', User::class);
$show_user = User::where('id',$id)->withTrashed()->first();
$assets = Asset::where('assigned_to', $id)->where('assigned_type', User::class)->with('model', 'model.category')->get();
$licenses = $show_user->licenses()->get();

View File

@@ -73,7 +73,7 @@ class ViewAssetsController extends Controller
$assets = Asset::with('model', 'defaultLoc', 'location', 'assignedTo', 'requests')->Hardware()->RequestableAssets()->get();
$models = AssetModel::with('category', 'requests', 'assets')->RequestableModels()->get();
return view('account/requestable-assets', compact('user', 'assets', 'models'));
return view('account/requestable-assets', compact('assets', 'models'));
}

View File

@@ -25,7 +25,7 @@ class Kernel extends HttpKernel
\Fideloper\Proxy\TrustProxies::class,
\App\Http\Middleware\CheckForSetup::class,
\App\Http\Middleware\CheckForDebug::class,
// \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**

View File

@@ -25,7 +25,7 @@ class AssetFileRequest extends Request
{
$max_file_size = \App\Helpers\Helper::file_upload_max_size();
return [
'file.*' => 'required|mimes:png,gif,jpg,jpeg,doc,docx,pdf,txt,zip,rar|max:'.$max_file_size,
'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,lic|max:'.$max_file_size,
];
}

View File

@@ -46,13 +46,14 @@ class AssetRequest extends Request
$rules['asset_tag'] = ($settings->auto_increment_assets == '1') ? 'max:255' : 'required';
$model = AssetModel::find($this->request->get('model_id'));
if($this->request->get('model_id') != '') {
$model = AssetModel::find($this->request->get('model_id'));
if (($model) && ($model->fieldset)) {
$rules += $model->fieldset->validation_rules();
if (($model) && ($model->fieldset)) {
$rules += $model->fieldset->validation_rules();
}
}
return $rules;
}

View File

@@ -43,6 +43,20 @@ class ItemImportRequest extends FormRequest
$import->save();
$fieldMappings=[];
if ($import->field_map) {
// This checks to make sure the field header has been mapped.
// If it hasn't been, it will throw an array_flip error
foreach ($import->field_map as $field => $fieldValue) {
$errorMessage = null;
if(is_null($fieldValue)){
$errorMessage = 'All import fields must be mapped.';
$this->errorCallback($import, $field, $errorMessage);
return $this->errors;
}
}
// We submit as csv field: column, but the importer is happier if we flip it here.
$fieldMappings = array_change_key_case(array_flip($import->field_map), CASE_LOWER);
// dd($fieldMappings);
@@ -50,6 +64,7 @@ class ItemImportRequest extends FormRequest
$importer->setCallbacks([$this, 'log'], [$this, 'progress'], [$this, 'errorCallback'])
->setUserId(Auth::id())
->setUpdating($this->has('import-update'))
->setShouldNotify($this->has('send-welcome'))
->setUsernameFormat('firstname.lastname')
->setFieldMappings($fieldMappings);
// $logFile = storage_path('logs/importer.log');
@@ -60,7 +75,7 @@ class ItemImportRequest extends FormRequest
public function log($string)
{
// \Log::Info($string);
\Log::Info($string);
}
public function progress($count)

View File

@@ -37,7 +37,7 @@ class SaveUserRequest extends Request
$rules['username'] = 'required_unless:ldap_import,1|string|min:1';
if ($this->request->get('ldap_import') == false)
{
$rules['password'] = Setting::passwordComplexityRulesSaving('store');
$rules['password'] = Setting::passwordComplexityRulesSaving('store').'|confirmed';
}
break;
}
@@ -46,7 +46,7 @@ class SaveUserRequest extends Request
case 'PUT':
$rules['first_name'] = 'required|string|min:1';
$rules['username'] = 'required_unless:ldap_import,1|string|min:1';
$rules['password'] = Setting::passwordComplexityRulesSaving('update');
$rules['password'] = Setting::passwordComplexityRulesSaving('update').'|confirmed';
break;
// Save only what's passed
@@ -58,9 +58,7 @@ class SaveUserRequest extends Request
default:break;
}
$rules['password_confirm'] = 'sometimes|required_with:password';
return $rules;
}

View File

@@ -29,8 +29,7 @@ class SetupUserRequest extends Request
'last_name' => 'required|string|min:1',
'username' => 'required|string|min:2|unique:users,username,NULL,deleted_at',
'email' => 'email|unique:users,email',
'password' => 'required|min:6',
'password_confirm' => 'required|min:6|same:password',
'password' => 'required|min:6|confirmed',
'email_domain' => 'required|min:4',
];
}

View File

@@ -26,6 +26,18 @@ class ActionlogsTransformer
if ($actionlog->filename!='') {
$icon = e(\App\Helpers\Helper::filetype_icon($actionlog->filename));
}
// This is necessary since we can't escape special characters within a JSON object
if (($actionlog->log_meta) && ($actionlog->log_meta!='')) {
$meta_array = json_decode($actionlog->log_meta);
foreach ($meta_array as $key => $value) {
foreach ($value as $meta_key => $meta_value) {
$clean_meta[$key][$meta_key] = e($meta_value);
}
}
}
$array = [
'id' => (int) $actionlog->id,
'icon' => $icon,
@@ -64,7 +76,7 @@ class ActionlogsTransformer
'note' => ($actionlog->note) ? e($actionlog->note): null,
'signature_file' => ($actionlog->accept_signature) ? route('log.signature.view', ['filename' => $actionlog->accept_signature ]) : null,
'log_meta' => ($actionlog->log_meta) ? json_decode($actionlog->log_meta): null,
'log_meta' => ((isset($clean_meta)) && (is_array($clean_meta))) ? $clean_meta: null,
];

View File

@@ -27,6 +27,11 @@ class AssetMaintenancesTransformer
'name'=> ($assetmaintenance->asset->name) ? e($assetmaintenance->asset->name) : null,
'asset_tag'=> e($assetmaintenance->asset->asset_tag)
] : null,
'company' => (($assetmaintenance->asset) && ($assetmaintenance->asset->company)) ? [
'id' => (int) $assetmaintenance->asset->company->id,
'name'=> ($assetmaintenance->asset->company->name) ? e($assetmaintenance->asset->company->name) : null,
] : null,
'title' => ($assetmaintenance->title) ? e($assetmaintenance->title) : null,
'location' => (($assetmaintenance->asset) && ($assetmaintenance->asset->location)) ? [
@@ -38,9 +43,9 @@ class AssetMaintenancesTransformer
'supplier' => ($assetmaintenance->supplier) ? ['id' => $assetmaintenance->supplier->id,'name'=> e($assetmaintenance->supplier->name)] : null,
'cost' => Helper::formatCurrencyOutput($assetmaintenance->cost),
'asset_maintenance_type' => e($assetmaintenance->asset_maintenance_type),
'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'datetime'),
'start_date' => Helper::getFormattedDateObject($assetmaintenance->start_date, 'date'),
'asset_maintenance_time' => $assetmaintenance->asset_maintenance_time,
'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'datetime'),
'completion_date' => Helper::getFormattedDateObject($assetmaintenance->completion_date, 'date'),
'user_id' => ($assetmaintenance->admin) ? ['id' => $assetmaintenance->admin->id,'name'=> e($assetmaintenance->admin->getFullNameAttribute())] : null,
'created_at' => Helper::getFormattedDateObject($assetmaintenance->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($assetmaintenance->updated_at, 'datetime'),

View File

@@ -2,6 +2,7 @@
namespace App\Http\Transformers;
use App\Models\LicenseSeat;
use App\Models\License;
use Gate;
use Illuminate\Database\Eloquent\Collection;
use App\Helpers\Helper;

View File

@@ -32,12 +32,6 @@ class SelectlistTransformer
}
// This is weird and awful, but the only way I can find to allow the user to
// clear the selection - @snipe
if (count($items_array) > 0) {
array_unshift($items_array, ['id' =>'', 'text'=> trans('general.clear_selection')]);
}
$results = [
'items' => $items_array,
'pagination' =>

View File

@@ -24,7 +24,7 @@ class UsersTransformer
$array = [
'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar),
'name' => e($user->first_name).' '.($user->last_name),
'name' => e($user->first_name).' '.e($user->last_name),
'first_name' => e($user->first_name),
'last_name' => e($user->last_name),
'username' => e($user->username),
@@ -53,6 +53,8 @@ class UsersTransformer
'permissions' => $user->decodePermissions(),
'activated' => ($user->activated =='1') ? true : false,
'two_factor_activated' => ($user->two_factor_active()) ? true : false,
'two_factor_enrolled' => ($user->two_factor_active_and_enrolled()) ? true : false,
'assets_count' => (int) $user->assets_count,
'licenses_count' => (int) $user->licenses_count,
'accessories_count' => (int) $user->accessories_count,

View File

@@ -4,6 +4,7 @@ namespace App\Importer;
use App\Models\CustomField;
use App\Models\Setting;
use App\Models\User;
use App\Models\Department;
use ForceUTF8\Encoding;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
@@ -29,6 +30,7 @@ abstract class Importer
*/
private $defaultFieldMap = [
'asset_tag' => 'asset tag',
'activated' => 'activated',
'category' => 'category',
'checkout_class' => 'checkout type', // Supports Location or User for assets. Using checkout_class instead of checkout_type because type exists on asset already.
'checkout_location' => 'checkout location',
@@ -60,7 +62,15 @@ abstract class Importer
'warranty_months' => 'warranty',
'full_name' => 'full name',
'email' => 'email',
'username' => 'username'
'username' => 'username',
'jobtitle' => 'job title',
'employee_num' => 'employee number',
'phone_number' => 'phone number',
'first_name' => 'first name',
'last_name' => 'last name',
'department' => 'department',
'manager_first_name' => 'manager first name',
'manager_last_name' => 'manager last name',
];
/**
* Map of item fields->csv names
@@ -95,7 +105,6 @@ abstract class Importer
if (! ini_get("auto_detect_line_endings")) {
ini_set("auto_detect_line_endings", '1');
}
// By default the importer passes a url to the file.
// However, for testing we also support passing a string directly
if (is_file($file)) {
@@ -113,24 +122,7 @@ abstract class Importer
$headerRow = $this->csv->fetchOne();
$results = $this->normalizeInputArray($this->csv->fetchAssoc());
// Stolen From https://adamwathan.me/2016/07/14/customizing-keys-when-mapping-collections/
// This 'inverts' the fields such that we have a collection of fields indexed by name.
$cFs = CustomField::All();
$this->customFields = $cFs->reduce(function ($nameLookup, $field) {
$nameLookup[$field['name']] = $field;
return $nameLookup;
});
// Remove any custom fields that do not exist in the header row. This prevents nulling out values that shouldn't exist.
// In detail, we compare the lower case name of custom fields (indexed by name) to the keys in the header row. This
// results in an array with only custom fields that are in the file.
if ($this->customFields) {
$this->customFields = array_intersect_key(
array_change_key_case($this->customFields),
array_change_key_case(array_flip($headerRow))
);
}
$this->populateCustomFields($headerRow);
DB::transaction(function () use (&$results) {
Model::unguard();
@@ -146,8 +138,35 @@ abstract class Importer
});
}
abstract protected function handle($row);
/**
* Fetch custom fields from database and translate/parse them into a format
* appropriate for use in the importer.
* @return void
* @author Daniel Meltzer
* @since 5.0
*/
protected function populateCustomFields($headerRow)
{
// Stolen From https://adamwathan.me/2016/07/14/customizing-keys-when-mapping-collections/
// This 'inverts' the fields such that we have a collection of fields indexed by name.
$this->customFields = CustomField::All()->reduce(function ($nameLookup, $field) {
$nameLookup[$field['name']] = $field;
return $nameLookup;
});
// Remove any custom fields that do not exist in the header row. This prevents nulling out values that shouldn't exist.
// In detail, we compare the lower case name of custom fields (indexed by name) to the keys in the header row. This
// results in an array with only custom fields that are in the file.
if ($this->customFields) {
$this->customFields = array_intersect_key(
array_change_key_case($this->customFields),
array_change_key_case(array_flip($headerRow))
);
}
}
/**
* Check to see if the given key exists in the array, and trim excess white space before returning it
*
@@ -162,10 +181,9 @@ abstract class Importer
{
$val = $default;
$key = $this->lookupCustomKey($key);
$this->log("Custom Key: ${key}");
// $this->log("Custom Key: ${key}");
if (array_key_exists($key, $array)) {
$val = Encoding::toUTF8(trim($array[ $key ]));
}
@@ -184,7 +202,6 @@ abstract class Importer
public function lookupCustomKey($key)
{
if (array_key_exists($key, $this->fieldMap)) {
// $this->log("Found a match in our custom map: {$key} is " . $this->fieldMap[$key]);
return $this->fieldMap[$key];
}
// Otherwise no custom key, return original.
@@ -234,88 +251,103 @@ abstract class Importer
}
/**
* Finds the user matching given data, or creates a new one if there is no match
* Finds the user matching given data, or creates a new one if there is no match.
* This is NOT used by the User Import, only for Asset/Accessory/etc where
* there are users listed and we have to create them and associate them at
* the same time. [ALG]
*
* @author Daniel Melzter
* @since 3.0
* @param $row array
* @return User Model w/ matching name
* @internal param string $user_username Username extracted from CSV
* @internal param string $user_email Email extracted from CSV
* @internal param string $first_name
* @internal param string $last_name
* @internal param array $user_array User details parsed from csv
*/
protected function createOrFetchUser($row)
{
$user_name = $this->findCsvMatch($row, "full_name");
$user_email = $this->findCsvMatch($row, "email");
$user_username = $this->findCsvMatch($row, "username");
$first_name = '';
$last_name = '';
if(empty($user_name) && empty($user_email) && empty($user_username)) {
$this->log('No user data provided - skipping user creation, just adding asset');
$user_array = [
'full_name' => $this->findCsvMatch($row, "full_name"),
'email' => $this->findCsvMatch($row, "email"),
'manager_id'=> '',
'department_id' => '',
'username' => $this->findCsvMatch($row, "username"),
'activated' => $this->fetchHumanBoolean($this->findCsvMatch($row, 'activated')),
];
\Log::debug('Importer.php Activated: '.$this->findCsvMatch($row, 'activated'));
// If the full name is empty, bail out--we need this to extract first name (at the very least)
if(empty($user_array['full_name'])) {
$this->log('Insufficient user data provided (Full name is required)- skipping user creation, just adding asset');
return false;
}
if( !empty($user_username)) {
// A username was given.
$user = User::where('username', $user_username)->first();
if($user) {
return $user;
// Is the user actually an ID?
if($user = $this->findUserByNumber($user_array['full_name'])) {
return $user;
}
$this->log('User does not appear to be an id with number: '.$user_array['full_name'].'. Continuing through our processes');
// Populate email if it does not exist.
if(empty($user_array['email'])) {
$user_array['email'] = User::generateEmailFromFullName($user_array['full_name']);
}
$user_formatted_array = User::generateFormattedNameFromFullName(Setting::getSettings()->username_format, $user_array['full_name']);
$user_array['first_name'] = $user_formatted_array['first_name'];
$user_array['last_name'] = $user_formatted_array['last_name'];
if (empty($user_array['username'])) {
$user_array['username'] = $user_formatted_array['username'];
if ($this->usernameFormat =='email') {
$user_array['username'] = $user_array['email'];
}
}
// Does this ever actually fire??
// Check for a matching user after trying to guess username.
if ($user = User::where('username', $user_array['username'])->first()) {
$this->log('User '.$user_array['username'].' already exists');
return $user;
}
// If at this point we have not found a username or first name, bail out in shame.
if(empty($user_array['username']) || empty($user_array['first_name'])) {
return false;
}
// No Luck, let's create one.
$user = new User;
$user->first_name = $user_array['first_name'];
$user->last_name = $user_array['last_name'];
$user->username = $user_array['username'];
$user->email = $user_array['email'];
$user->manager_id = (isset($user_array['manager_id']) ? $user_array['manager_id'] : null);
$user->department_id = (isset($user_array['department_id']) ? $user_array['department_id']: null);
$user->activated = $user_array['activated'];
$user->password = $this->tempPassword;
\Log::debug('Creating a user with the following attributes: '.print_r($user_array, true));
if ($user->save()) {
\Log::debug('Importer.php Name: '.$user->first_name.' '.$user->last_name.' ('.$user->username.')');
$this->log('User '.$user_array['username'].' created');
return $user;
}
$this->logError($user, 'User "' . $user_array['username'] . '" was not able to be created.');
return false;
}
/**
* Matches a user by user_id if user_name provided is a number
* @param string $user_name users full name from csv
* @return User User Matching ID
*/
protected function findUserByNumber($user_name)
{
// A number was given instead of a name
if (is_numeric($user_name)) {
$this->log('User '.$user_name.' is not a name - assume this user already exists');
$user = User::find($user_name);
if($user) {
return $user;
}
$this->log('User with id'.$user_name.' does not exist. Continuing through our processes');
$this->log('User '.$user_name.' is a number - lets see if it matches a user id');
return User::find($user_name);
}
// Generate data based on user name.
$user_email_array = User::generateFormattedNameFromFullName(Setting::getSettings()->email_format, $user_name);
$first_name = $user_email_array['first_name'];
$last_name = $user_email_array['last_name'];
if (empty($user_email)) {
if (Setting::getSettings()->email_domain) {
$user_email = str_slug($user_email_array['username']).'@'.Setting::getSettings()->email_domain;
}
}
if (empty($user_username)) {
if ($this->usernameFormat =='email') {
$user_username = $user_email;
} else {
$user_name_array = User::generateFormattedNameFromFullName(Setting::getSettings()->username_format, $user_name);
$user_username = $user_name_array['username'];
}
}
$user = new User;
if (!empty($user_username)) {
if ($user = User::MatchEmailOrUsername($user_username, $user_email)
->whereNotNull('username')->first()) {
$this->log('User '.$user_username.' already exists');
} elseif (( $first_name != '') && ($last_name != '') && ($user_username != '')) {
$user = new User;
$user->first_name = $first_name;
$user->last_name = $last_name;
$user->username = $user_username;
$user->email = $user_email;
$user->activated = 1;
$user->password = $this->tempPassword;
if ($user->save()) {
$this->log('User '.$first_name.' created');
} else {
$this->logError($user, 'User "' . $first_name . '"');
}
}
}
return $user;
}
/**
@@ -346,6 +378,20 @@ abstract class Importer
return $this;
}
/**
* Sets the Are we updating items in the import.
*
* @param bool $updating the updating
*
* @return self
*/
public function setShouldNotify($send_welcome)
{
$this->send_welcome = $send_welcome;
return $this;
}
/**
* Defines mappings of csv fields
*
@@ -393,4 +439,64 @@ abstract class Importer
return $this;
}
public function fetchHumanBoolean($value)
{
if (($value =='1') || (strtolower($value) =='true') || (strtolower($value) =='yes'))
{
return '1';
}
return '0';
}
/**
* Fetch an existing department, or create new if it doesn't exist
*
* @author A. Gianotto
* @since 4.6.5
* @param $user_department string
* @return int id of company created/found
*/
public function createOrFetchDepartment($user_department_name)
{
if ($user_department_name!='') {
$department = Department::where('name', '=', $user_department_name)->first();
if ($department) {
$this->log('A matching Department ' . $user_department_name . ' already exists');
return $department->id;
}
$department = new Department();
$department->name = $user_department_name;
if ($department->save()) {
$this->log('Department ' . $user_department_name . ' was created');
return $department->id;
}
$this->logError($department, 'Department');
}
return null;
}
/**
* Fetch an existing manager
*
* @author A. Gianotto
* @since 4.6.5
* @param $user_manager string
* @return int id of company created/found
*/
public function fetchManager($user_manager_first_name, $user_manager_last_name)
{
$manager = User::where('first_name', '=', $user_manager_first_name)
->where('last_name', '=', $user_manager_last_name)->first();
if ($manager) {
$this->log('A matching Manager ' . $user_manager_first_name . ' '. $user_manager_last_name . ' already exists');
return $manager->id;
}
$this->log('No matching Manager ' . $user_manager_first_name . ' '. $user_manager_last_name . ' found. If their user account is being created through this import, you should re-process this file again. ');
return null;
}
}

View File

@@ -10,6 +10,7 @@ use App\Models\Location;
use App\Models\Manufacturer;
use App\Models\Statuslabel;
use App\Models\Supplier;
use App\Models\Department;
use App\Models\User;
class ItemImporter extends Importer
@@ -54,6 +55,18 @@ class ItemImporter extends Importer
if ($this->shouldUpdateField($item_supplier)) {
$this->item['supplier_id'] = $this->createOrFetchSupplier($item_supplier);
}
$item_department = $this->findCsvMatch($row, "department");
if ($this->shouldUpdateField($item_department)) {
$this->item['department_id'] = $this->createOrFetchDepartment($item_department);
}
$item_manager_first_name = $this->findCsvMatch($row, "manager_first_name");
$item_manager_last_name = $this->findCsvMatch($row, "manager_last_name");
if ($this->shouldUpdateField($item_manager_first_name)) {
$this->item['manager_id'] = $this->fetchManager($item_manager_first_name, $item_manager_last_name);
}
$this->item["name"] = $this->findCsvMatch($row, "item_name");
$this->item["notes"] = $this->findCsvMatch($row, "notes");
$this->item["order_number"] = $this->findCsvMatch($row, "order_number");
@@ -84,15 +97,13 @@ class ItemImporter extends Importer
*/
protected function determineCheckout($row)
{
// We only supporty checkout-to-location for asset, so short circuit otherw.
// We only support checkout-to-location for asset, so short circuit otherwise.
if(get_class($this) != AssetImporter::class) {
return $this->createOrFetchUser($row);
}
if ($this->item['checkout_class'] === 'location') {
// dd($this->findCsvMatch($row, 'checkout_location'));
return Location::findOrFail($this->createOrFetchLocation($this->findCsvMatch($row, 'checkout_location')));
// dd('here');
}
return $this->createOrFetchUser($row);
@@ -163,7 +174,7 @@ class ItemImporter extends Importer
* @since 3.0
* @param array
* @param $category Category
* @param $manufacturer Manufacturer
* @param $row Manufacturer
* @return int Id of asset model created/found
* @internal param $asset_modelno string
*/
@@ -281,6 +292,8 @@ class ItemImporter extends Importer
return null;
}
/**
* Fetch the existing status label or create new if it doesn't exist.
*

View File

@@ -4,14 +4,23 @@ namespace App\Importer;
use App\Helpers\Helper;
use App\Models\User;
use App\Notifications\WelcomeNotification;
/**
* This is ONLY used for the User Import. When we are importing users
* via an Asset/etc import, we use createOrFetchUser() in
* App\Importer.php. [ALG]
*
* Class UserImporter
* @package App\Importer
*
*/
class UserImporter extends ItemImporter
{
protected $users;
public function __construct($filename)
{
parent::__construct($filename);
// $this->users = User::all();
}
protected function handle($row)
@@ -26,28 +35,44 @@ class UserImporter extends ItemImporter
*
* @author Daniel Melzter
* @since 4.0
* @param array $row
*/
public function createUserIfNotExists(array $row)
{
// User Specific Bits
// Pull the records from the CSV to determine their values
$this->item['username'] = $this->findCsvMatch($row, 'username');
$this->item['first_name'] = $this->findCsvMatch($row, 'first_name');
$this->item['last_name'] = $this->findCsvMatch($row, 'last_name');
\Log::debug('UserImporter.php Name: '.$this->item['first_name'].' '.$this->item['last_name'].' ('.$this->item['username'].')');
$this->item['email'] = $this->findCsvMatch($row, 'email');
$this->item['phone'] = $this->findCsvMatch($row, 'phone_number');
$this->item['jobtitle'] = $this->findCsvMatch($row, 'jobtitle');
$this->item['activated'] = ($this->fetchHumanBoolean($this->findCsvMatch($row, 'activated')) == 1) ? '1' : 0;
\Log::debug('UserImporter.php Activated: '.$this->findCsvMatch($row, 'activated'));
\Log::debug('UserImporter.php Activated fetchHumanBoolean: '. $this->fetchHumanBoolean($this->findCsvMatch($row, 'activated')));
$this->item['employee_num'] = $this->findCsvMatch($row, 'employee_num');
$this->item['department_id'] = $this->createOrFetchDepartment($this->findCsvMatch($row, 'department')) ? $this->createOrFetchDepartment($this->findCsvMatch($row, 'department')) : null;
$this->item['manager_id'] = $this->fetchManager($this->findCsvMatch($row, 'manager_first_name'), $this->findCsvMatch($row, 'manager_last_name')) ? $this->fetchManager($this->findCsvMatch($row, 'manager_first_name'), $this->findCsvMatch($row, 'manager_last_name')) : null;
$user = User::where('username', $this->item['username'])->first();
if ($user) {
if (!$this->updating) {
$this->log('A matching User ' . $this->item["name"] . ' already exists. ');
\Log::debug('A matching User ' . $this->item["name"] . ' already exists. ');
return;
}
$this->log('Updating User');
$user->update($this->sanitizeItemForUpdating($user));
$user->save();
// \Log::debug('UserImporter.php Updated User ' . print_r($user, true));
return;
}
// This needs to be applied after the update logic, otherwise we'll overwrite user passwords
// Issue #5408
$this->item['password'] = $this->tempPassword;
@@ -57,12 +82,27 @@ class UserImporter extends ItemImporter
$user->fill($this->sanitizeItemForStoring($user));
if ($user->save()) {
// $user->logCreate('Imported using CSV Importer');
$this->log("User " . $this->item["name"] . ' was created');
if(($user->email) && ($user->activated=='1')) {
$data = [
'email' => $user->email,
'username' => $user->username,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'password' => $this->tempPassword,
];
if ($this->send_welcome) {
$user->notify(new WelcomeNotification($data));
}
}
$user = null;
$this->item = null;
return;
}
$this->logError($user, 'User');
return;
}

View File

@@ -1,16 +1,20 @@
| CSV | Item | Applicable Types |
|---------------------|------------------|-------------------------------------------|
| activated | | User |
| asset tag | asset_tag | Asset |
| category | category | All |
| company | company | All |
| department_id | | User ? All |
| item name | item_name | All |
| image | image | asset |
| image | image | Asset |
| email | | |
| expiration date | expiration_date | License |
| location | location | All |
| notes | notes | All |
| licensed to email | license_email | License |
| licensed to name | license_name | License |
| maintained | maintained | License |
| manager_id | | User |
| manufacturer | manufacturer | All |
| model name | asset_model | Asset |
| model number | model_number | Asset |
@@ -22,12 +26,12 @@
| reassignable | reassignable | License |
| requestable | requestable | Asset, Accessory? |
| seats | seats | License |
| serial number | serial | asset, license |
| status | status | asset ? All |
| serial number | serial | Asset, license |
| status | status | Asset ? All |
| supplier | supplier | Asset ? All |
| termination date | termination_date | License |
| warranty months | warranty_months | asset |
| warranty months | warranty_months | Asset |
| User Related Fields | assigned_to | Asset |
| name | | |
| email | | |
| username | | |
| username | | |

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Failed;
use Illuminate\Http\Request;
use DB;
use Carbon\Carbon;
class LogFailedLogin
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param \Illuminate\Auth\Events\Failed $event
* @return void
*/
public function handle(Failed $event)
{
$now = new Carbon();
try {
DB::table('login_attempts')->insert(
[
'username' => $event->credentials['username'],
'user_agent' => request()->header('User-Agent'),
'remote_ip' => request()->ip(),
'successful' => 0,
'created_at' => $now,
]
);
} catch (\Exception $e) {
\Log::debug($e);
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
use Illuminate\Http\Request;
use DB;
use Carbon\Carbon;
class LogSuccessfulLogin
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param Login $event
* @return void
*/
public function handle(Login $event)
{
$now = new Carbon();
try {
DB::table('login_attempts')->insert(
[
'username' => $event->user->username,
'user_agent' => request()->header('User-Agent'),
'remote_ip' => request()->ip(),
'successful' => 1,
'created_at' => $now,
]
);
} catch (\Exception $e) {
\Log::debug($e);
}
}
}

View File

@@ -1,6 +1,7 @@
<?php
namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Watson\Validating\ValidatingTrait;
@@ -25,6 +26,28 @@ class Accessory extends SnipeModel
'requestable' => 'boolean'
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'model_number', 'order_number', 'purchase_date'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'category' => ['name'],
'company' => ['name'],
'manufacturer' => ['name'],
'supplier' => ['name'],
'location' => ['name']
];
/**
* Set static properties to determine which checkout/checkin handlers we should use
*/
@@ -171,40 +194,6 @@ class Accessory extends SnipeModel
return $remaining;
}
/**
* Query builder scope to search on text
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->whereHas('category', function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%');
})->orWhere(function ($query) use ($search) {
$query->whereHas('company', function ($query) use ($search) {
$query->where('companies.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('manufacturer', function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('location', function ($query) use ($search) {
$query->where('locations.name', 'LIKE', '%'.$search.'%');
});
})->orWhere('accessories.name', 'LIKE', '%'.$search.'%')
->orWhere('accessories.model_number', 'LIKE', '%'.$search.'%')
->orWhere('accessories.order_number', 'LIKE', '%'.$search.'%');
});
}
/**
* Query builder scope to order on company
*

View File

@@ -1,5 +1,6 @@
<?php
namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -24,6 +25,24 @@ class Actionlog extends SnipeModel
public $timestamps = true;
protected $fillable = [ 'created_at', 'item_type','user_id','item_id','action_type','note','target_id', 'target_type' ];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['action_type', 'note', 'log_meta'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'company' => ['name']
];
// Overridden from Builder to automatically add the company
public static function boot()
{
@@ -200,31 +219,4 @@ class Actionlog extends SnipeModel
->orderBy('created_at', 'asc')
->get();
}
/**
* Query builder scope to search on text for complex Bootstrap Tables API
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
$search = explode(' OR ', $search);
return $query->where(function ($query) use ($search) {
foreach ($search as $search) {
$query->where(function ($query) use ($search) {
$query->whereHas('company', function ($query) use ($search) {
$query->where('companies.name', 'LIKE', '%'.$search.'%');
});
})->orWhere('action_type', 'LIKE', '%'.$search.'%')
->orWhere('note', 'LIKE', '%'.$search.'%')
->orWhere('log_meta', 'LIKE', '%'.$search.'%');
}
});
}
}

View File

@@ -4,11 +4,13 @@ namespace App\Models;
use App\Exceptions\CheckoutNotAllowed;
use App\Http\Traits\UniqueSerialTrait;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use AssetPresenter;
use Auth;
use Carbon\Carbon;
use Config;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;
use Log;
use Watson\Validating\ValidatingTrait;
@@ -30,6 +32,7 @@ class Asset extends Depreciable
const ASSET = 'asset';
const USER = 'user';
const ACCEPTANCE_PENDING = 'pending';
/**
* Set static properties to determine which checkout/checkin handlers we should use
*/
@@ -110,7 +113,42 @@ class Asset extends Depreciable
'warranty_months',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = [
'name',
'asset_tag',
'serial',
'order_number',
'purchase_cost',
'notes',
'created_at',
'updated_at',
'purchase_date',
'expected_checkin',
'next_audit_date',
'last_audit_date'
];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'assetstatus' => ['name'],
'supplier' => ['name'],
'company' => ['name'],
'defaultLoc' => ['name'],
'model' => ['name', 'model_number'],
'model.category' => ['name'],
'model.manufacturer' => ['name'],
];
public function getDisplayNameAttribute()
{
@@ -190,8 +228,22 @@ class Asset extends Depreciable
if($target->location) {
$this->location_id = $target->location->id;
}
if($target instanceof Location) {
$this->location_id = $target->id;
}
}
/**
* Does the user have to confirm that they accept the asset?
*
* If so, set the acceptance-status to "pending".
* This value is used in the unaccepted assets reports, for example
*
* @see https://github.com/snipe/snipe-it/issues/5772
*/
if ($this->requireAcceptance() && $target instanceof User) {
$this->accepted = self::ACCEPTANCE_PENDING;
}
if ($this->save()) {
$this->logCheckout($note, $target);
@@ -568,6 +620,64 @@ class Asset extends Depreciable
}
}
/**
* Run additional, advanced searches.
*
* @param Illuminate\Database\Eloquent\Builder $query
* @param array $terms The search terms
* @return Illuminate\Database\Eloquent\Builder
*/
public function advancedTextSearch(Builder $query, array $terms) {
/**
* Assigned user
*/
$query = $query->leftJoin('users as assets_users',function ($leftJoin) {
$leftJoin->on("assets_users.id", "=", "assets.assigned_to")
->where("assets.assigned_type", "=", User::class);
});
foreach($terms as $term) {
$query = $query
->orWhere('assets_users.first_name', 'LIKE', '%'.$term.'%')
->orWhere('assets_users.last_name', 'LIKE', '%'.$term.'%')
->orWhere('assets_users.username', 'LIKE', '%'.$term.'%')
->orWhereRaw('CONCAT('.DB::getTablePrefix().'assets_users.first_name," ",'.DB::getTablePrefix().'assets_users.last_name) LIKE ?', ["%$term%", "%$term%"]);
}
/**
* Assigned location
*/
$query = $query->leftJoin('locations as assets_locations',function ($leftJoin) {
$leftJoin->on("assets_locations.id","=","assets.assigned_to")
->where("assets.assigned_type","=",Location::class);
});
foreach($terms as $term) {
$query = $query->orWhere('assets_locations.name', 'LIKE', '%'.$term.'%');
}
/**
* Assigned assets
*/
$query = $query->leftJoin('assets as assigned_assets',function ($leftJoin) {
$leftJoin->on('assigned_assets.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', Asset::class);
});
foreach($terms as $term) {
$query = $query->orWhere('assigned_assets.name', 'LIKE', '%'.$term.'%');
}
return $query;
}
/**
* -----------------------------------------------
@@ -686,6 +796,83 @@ class Asset extends Depreciable
});
}
/**
* Query builder scope for Assets that are due for auditing, based on the assets.next_audit_date
* and settings.audit_warning_days.
*
* This is/will be used in the artisan command snipeit:upcoming-audits and also
* for an upcoming API call for retrieving a report on assets that will need to be audited.
*
* Due for audit soon:
* next_audit_date greater than or equal to now (must be in the future)
* and (next_audit_date - threshold days) <= now ()
*
* Example:
* next_audit_date = May 4, 2025
* threshold for alerts = 30 days
* now = May 4, 2019
*
* @author A. Gianotto <snipe@snipe.net>
* @since v4.6.16
* @param Setting $settings
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeDueForAudit($query, $settings)
{
return $query->whereNotNull('assets.next_audit_date')
->where('assets.next_audit_date', '>=', Carbon::now())
->whereRaw("DATE_SUB(assets.next_audit_date, INTERVAL $settings->audit_warning_days DAY) <= '".Carbon::now()."'")
->where('assets.archived', '=', 0)
->NotArchived();
}
/**
* Query builder scope for Assets that are OVERDUE for auditing, based on the assets.next_audit_date
* and settings.audit_warning_days. It checks to see if assets.next audit_date is before now
*
* This is/will be used in the artisan command snipeit:upcoming-audits and also
* for an upcoming API call for retrieving a report on overdue assets.
*
* @author A. Gianotto <snipe@snipe.net>
* @since v4.6.16
* @param Setting $settings
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOverdueForAudit($query)
{
return $query->whereNotNull('assets.next_audit_date')
->where('assets.next_audit_date', '<', Carbon::now())
->where('assets.archived', '=', 0)
->NotArchived();
}
/**
* Query builder scope for Assets that are due for auditing OR overdue, based on the assets.next_audit_date
* and settings.audit_warning_days.
*
* This is/will be used in the artisan command snipeit:upcoming-audits and also
* for an upcoming API call for retrieving a report on assets that will need to be audited.
*
* @author A. Gianotto <snipe@snipe.net>
* @since v4.6.16
* @param Setting $settings
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeDueOrOverdueForAudit($query, $settings)
{
return $query->whereNotNull('assets.next_audit_date')
->whereRaw("DATE_SUB(assets.next_audit_date, INTERVAL $settings->audit_warning_days DAY) <= '".Carbon::now()."'")
->where('assets.archived', '=', 0)
->NotArchived();
}
/**
* Query builder scope for Archived assets
*
@@ -801,83 +988,6 @@ class Asset extends Depreciable
return $query->where("accepted", "=", "accepted");
}
/**
* Query builder scope to search on text for complex Bootstrap Tables API.
* This is really horrible, but I can't think of a less-awful way to do it.
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
$search = explode(' OR ', $search);
return $query->leftJoin('users as assets_users',function ($leftJoin) {
$leftJoin->on("assets_users.id", "=", "assets.assigned_to")
->where("assets.assigned_type", "=", User::class);
})->leftJoin('locations as assets_locations',function ($leftJoin) {
$leftJoin->on("assets_locations.id","=","assets.assigned_to")
->where("assets.assigned_type","=",Location::class);
})->leftJoin('assets as assigned_assets',function ($leftJoin) {
$leftJoin->on('assigned_assets.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', Asset::class);
})->where(function ($query) use ($search) {
foreach ($search as $search) {
$query->whereHas('model', function ($query) use ($search) {
$query->whereHas('category', function ($query) use ($search) {
$query->where(function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%')
->orWhere('models.name', 'LIKE', '%'.$search.'%')
->orWhere('models.model_number', 'LIKE', '%'.$search.'%');
});
});
})->orWhereHas('model', function ($query) use ($search) {
$query->whereHas('manufacturer', function ($query) use ($search) {
$query->where(function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
});
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('assetstatus', function ($query) use ($search) {
$query->where('status_labels.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('supplier', function ($query) use ($search) {
$query->where('suppliers.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('company', function ($query) use ($search) {
$query->where('companies.name', 'LIKE', '%' . $search . '%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('defaultLoc', function ($query) use ($search) {
$query->where('locations.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->where('assets_users.first_name', 'LIKE', '%'.$search.'%')
->orWhere('assets_users.last_name', 'LIKE', '%'.$search.'%')
->orWhereRaw('CONCAT('.DB::getTablePrefix().'assets_users.first_name," ",'.DB::getTablePrefix().'assets_users.last_name) LIKE ?', ["%$search%", "%$search%"])
->orWhere('assets_users.username', 'LIKE', '%'.$search.'%')
->orWhere('assets_locations.name', 'LIKE', '%'.$search.'%')
->orWhere('assigned_assets.name', 'LIKE', '%'.$search.'%');
})->orWhere('assets.name', 'LIKE', '%'.$search.'%')
->orWhere('assets.asset_tag', 'LIKE', '%'.$search.'%')
->orWhere('assets.serial', 'LIKE', '%'.$search.'%')
->orWhere('assets.order_number', 'LIKE', '%'.$search.'%')
->orWhere('assets.purchase_cost', 'LIKE', '%'.$search.'%')
->orWhere('assets.notes', 'LIKE', '%'.$search.'%');
}
foreach (CustomField::all() as $field) {
$query->orWhere('assets.'.$field->db_column_name(), 'LIKE', "%$search%");
}
})->withTrashed()->whereNull("assets.deleted_at"); //workaround for laravel bug
}
/**
* Query builder scope to search on text for complex Bootstrap Tables API.
*
@@ -1147,8 +1257,8 @@ class Asset extends Depreciable
*/
public function scopeInCategory($query, $category_id)
{
return $query->join('models', 'assets.model_id', '=', 'models.id')
->join('categories', 'models.category_id', '=', 'categories.id')->where('models.category_id', '=', $category_id);
return $query->join('models as category_models', 'assets.model_id', '=', 'category_models.id')
->join('categories', 'category_models.category_id', '=', 'categories.id')->where('category_models.category_id', '=', $category_id);
}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Helpers\Helper;
use App\Models\Traits\Searchable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Lang;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -19,7 +20,7 @@ class AssetMaintenance extends Model implements ICompanyableChild
use ValidatingTrait;
protected $dates = [ 'deleted_at' ];
protected $dates = [ 'deleted_at', 'start_date' , 'completion_date'];
protected $table = 'asset_maintenances';
// Declaring rules for form validation
protected $rules = [
@@ -28,12 +29,29 @@ class AssetMaintenance extends Model implements ICompanyableChild
'asset_maintenance_type' => 'required',
'title' => 'required|max:100',
'is_warranty' => 'boolean',
'start_date' => 'required|date_format:"Y-m-d"',
'completion_date' => 'nullable|date_format:"Y-m-d"',
'start_date' => 'required|date',
'completion_date' => 'nullable|date',
'notes' => 'string|nullable',
'cost' => 'numeric|nullable'
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['title', 'notes', 'asset_maintenance_type', 'cost', 'start_date', 'completion_date'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
public function getCompanyableParents()
{
return [ 'asset' ];
@@ -54,6 +72,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
trans('admin/asset_maintenances/general.repair') => trans('admin/asset_maintenances/general.repair'),
trans('admin/asset_maintenances/general.upgrade') => trans('admin/asset_maintenances/general.upgrade'),
'PAT test' => 'PAT test',
trans('admin/asset_maintenances/general.calibration') => trans('admin/asset_maintenances/general.calibration'),
'PAT test' => 'PAT test',
];
}
@@ -139,30 +159,8 @@ class AssetMaintenance extends Model implements ICompanyableChild
* -----------------------------------------------
* BEGIN QUERY SCOPES
* -----------------------------------------------
**/
**/
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('asset_maintenances.title', 'LIKE', '%'.$search.'%')
->orWhere('asset_maintenances.notes', 'LIKE', '%'.$search.'%')
->orWhere('asset_maintenances.asset_maintenance_type', 'LIKE', '%'.$search.'%')
->orWhere('asset_maintenances.cost', 'LIKE', '%'.$search.'%')
->orWhere('asset_maintenances.start_date', 'LIKE', '%'.$search.'%')
->orWhere('asset_maintenances.completion_date', 'LIKE', '%'.$search.'%');
});
}
/**
* Query builder scope to order on admin user

View File

@@ -3,6 +3,7 @@ namespace App\Models;
use App\Models\Requestable;
use App\Models\SnipeModel;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -68,6 +69,26 @@ class AssetModel extends SnipeModel
'user_id',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'model_number', 'notes', 'eol'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'depreciation' => ['name'],
'category' => ['name'],
'manufacturer' => ['name'],
];
public function assets()
{
return $this->hasMany('\App\Models\Asset', 'model_id');
@@ -160,38 +181,7 @@ class AssetModel extends SnipeModel
{
return $query->where('requestable', '1');
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where('models.name', 'LIKE', "%$search%")
->orWhere('model_number', 'LIKE', "%$search%")
->orWhere(function ($query) use ($search) {
$query->whereHas('depreciation', function ($query) use ($search) {
$query->where('depreciations.name', 'LIKE', '%'.$search.'%');
});
})
->orWhere(function ($query) use ($search) {
$query->whereHas('category', function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%');
});
})
->orWhere(function ($query) use ($search) {
$query->whereHas('manufacturer', function ($query) use ($search) {
$query->where('manufacturers.name', 'LIKE', '%'.$search.'%');
});
});
}
}
/**
* Query builder scope to search on text, including catgeory and manufacturer name

View File

@@ -3,6 +3,7 @@ namespace App\Models;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\SnipeModel;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -63,6 +64,21 @@ class Category extends SnipeModel
'user_id',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'category_type'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
public function has_models()
{
@@ -143,22 +159,4 @@ class Category extends SnipeModel
return $query->where('require_acceptance', '=', true);
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('name', 'LIKE', '%'.$search.'%')
->orWhere('category_type', 'LIKE', '%'.$search.'%');
});
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Models\SnipeModel;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Auth;
use DB;
@@ -35,6 +36,21 @@ final class Company extends SnipeModel
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'created_at', 'updated_at'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [];
/**
* The attributes that are mass assignable.
@@ -192,20 +208,4 @@ final class Company extends SnipeModel
{
return $this->hasMany(Component::class, 'company_id');
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
return $query->where(function ($query) use ($search) {
$query->where('name', 'LIKE', '%'.$search.'%');
});
}
}

View File

@@ -1,6 +1,7 @@
<?php
namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Watson\Validating\ValidatingTrait;
@@ -67,6 +68,26 @@ class Component extends SnipeModel
'serial',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableAttributes = ['name', 'order_number', 'serial', 'purchase_cost', 'purchase_date'];
/**
* The relations and their attributes that should be included when searching the model.
*
* @var array
*/
protected $searchableRelations = [
'category' => ['name'],
'company' => ['name'],
'location' => ['name'],
];
public function location()
{
return $this->belongsTo('\App\Models\Location', 'location_id');
@@ -114,49 +135,7 @@ class Component extends SnipeModel
$total = $this->qty;
$remaining = $total - $checkedout;
return $remaining;
}
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
/**
* Query builder scope to search on text
*
* @param Illuminate\Database\Query\Builder $query Query builder instance
* @param text $search Search term
*
* @return Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeTextSearch($query, $search)
{
$search = explode(' ', $search);
return $query->where(function ($query) use ($search) {
foreach ($search as $search) {
$query->whereHas('category', function ($query) use ($search) {
$query->where('categories.name', 'LIKE', '%'.$search.'%');
})->orWhere(function ($query) use ($search) {
$query->whereHas('company', function ($query) use ($search) {
$query->where('companies.name', 'LIKE', '%'.$search.'%');
});
})->orWhere(function ($query) use ($search) {
$query->whereHas('location', function ($query) use ($search) {
$query->where('locations.name', 'LIKE', '%'.$search.'%');
});
})->orWhere('components.name', 'LIKE', '%'.$search.'%')
->orWhere('components.order_number', 'LIKE', '%'.$search.'%')
->orWhere('components.serial', 'LIKE', '%'.$search.'%')
->orWhere('components.purchase_cost', 'LIKE', '%'.$search.'%');
}
});
}
}
/**
* Query builder scope to order on company

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