Merge branch 'develop' into snipeit_v7_laravel10

This commit is contained in:
Brady Wetherington 2023-11-27 13:04:39 +00:00
commit 6210716199
52 changed files with 2349 additions and 2959 deletions

View file

@ -2979,6 +2979,24 @@
"contributions": [
"code"
]
},
{
"login": "Azooz2014",
"name": "Abdelaziz Faki",
"avatar_url": "https://avatars.githubusercontent.com/u/7429229?v=4",
"profile": "https://azooz2014.github.io/",
"contributions": [
"code"
]
},
{
"login": "bilias",
"name": "bilias",
"avatar_url": "https://avatars.githubusercontent.com/u/47315739?v=4",
"profile": "https://github.com/bilias",
"contributions": [
"code"
]
}
]
}

View file

@ -1,5 +1,5 @@
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Twitter Follow](https://img.shields.io/twitter/follow/snipeitapp.svg?style=social)](https://twitter.com/snipeitapp) [![Codacy Badge](https://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-328-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev)
[![All Contributors](https://img.shields.io/badge/all_contributors-330-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev)
## Snipe-IT - Open Source Asset Management System
@ -145,7 +145,8 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/28321?v=4" width="110px;"/><br /><sub>Chris Hartjes</sub>](http://www.littlehart.net/atthekeyboard)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chartjes "Code") | [<img src="https://avatars.githubusercontent.com/u/2404584?v=4" width="110px;"/><br /><sub>geo-chen</sub>](https://github.com/geo-chen)<br />[💻](https://github.com/snipe/snipe-it/commits?author=geo-chen "Code") | [<img src="https://avatars.githubusercontent.com/u/6006620?v=4" width="110px;"/><br /><sub>Phan Nguyen</sub>](https://github.com/nh314)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nh314 "Code") | [<img src="https://avatars.githubusercontent.com/u/115993812?v=4" width="110px;"/><br /><sub>Iisakki Jaakkola</sub>](https://github.com/StarlessNights)<br />[💻](https://github.com/snipe/snipe-it/commits?author=StarlessNights "Code") | [<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="110px;"/><br /><sub>Ikko Ashimine</sub>](https://bandism.net/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=eltociear "Code") | [<img src="https://avatars.githubusercontent.com/u/56871540?v=4" width="110px;"/><br /><sub>Lukas Fehling</sub>](https://github.com/lukasfehling)<br />[💻](https://github.com/snipe/snipe-it/commits?author=lukasfehling "Code") | [<img src="https://avatars.githubusercontent.com/u/1975990?v=4" width="110px;"/><br /><sub>Fernando Almeida</sub>](https://github.com/fernando-almeida)<br />[💻](https://github.com/snipe/snipe-it/commits?author=fernando-almeida "Code") |
| [<img src="https://avatars.githubusercontent.com/u/116301219?v=4" width="110px;"/><br /><sub>akemidx</sub>](https://github.com/akemidx)<br />[💻](https://github.com/snipe/snipe-it/commits?author=akemidx "Code") | [<img src="https://avatars.githubusercontent.com/u/144778?v=4" width="110px;"/><br /><sub>Oguz Bilgic</sub>](http://oguz.site)<br />[💻](https://github.com/snipe/snipe-it/commits?author=oguzbilgic "Code") | [<img src="https://avatars.githubusercontent.com/u/9262438?v=4" width="110px;"/><br /><sub>Scooter Crawford</sub>](https://github.com/scoo73r)<br />[💻](https://github.com/snipe/snipe-it/commits?author=scoo73r "Code") | [<img src="https://avatars.githubusercontent.com/u/5957345?v=4" width="110px;"/><br /><sub>subdriven</sub>](https://github.com/subdriven)<br />[💻](https://github.com/snipe/snipe-it/commits?author=subdriven "Code") | [<img src="https://avatars.githubusercontent.com/u/658865?v=4" width="110px;"/><br /><sub>Andrew Savinykh</sub>](https://github.com/AndrewSav)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AndrewSav "Code") | [<img src="https://avatars.githubusercontent.com/u/1155067?v=4" width="110px;"/><br /><sub>Tadayuki Onishi</sub>](https://kenchan0130.github.io)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kenchan0130 "Code") | [<img src="https://avatars.githubusercontent.com/u/112496896?v=4" width="110px;"/><br /><sub>Florian</sub>](https://github.com/floschoepfer)<br />[💻](https://github.com/snipe/snipe-it/commits?author=floschoepfer "Code") |
| [<img src="https://avatars.githubusercontent.com/u/7305753?v=4" width="110px;"/><br /><sub>Spencer Long</sub>](http://spencerlong.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=spencerrlongg "Code") | [<img src="https://avatars.githubusercontent.com/u/1141514?v=4" width="110px;"/><br /><sub>Marcus Moore</sub>](https://github.com/marcusmoore)<br />[💻](https://github.com/snipe/snipe-it/commits?author=marcusmoore "Code") | [<img src="https://avatars.githubusercontent.com/u/570639?v=4" width="110px;"/><br /><sub>Martin Meredith</sub>](https://github.com/Mezzle)<br /> | [<img src="https://avatars.githubusercontent.com/u/5731963?v=4" width="110px;"/><br /><sub>dboth</sub>](http://dboth.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dboth "Code") | [<img src="https://avatars.githubusercontent.com/u/87536651?v=4" width="110px;"/><br /><sub>Zachary Fleck</sub>](https://github.com/zacharyfleck)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zacharyfleck "Code") | [<img src="https://avatars.githubusercontent.com/u/74609912?v=4" width="110px;"/><br /><sub>VIKAAS-A</sub>](https://github.com/vikaas-cyper)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vikaas-cyper "Code") | [<img src="https://avatars.githubusercontent.com/u/88882041?v=4" width="110px;"/><br /><sub>Abdul Kareem</sub>](https://github.com/ak-piracha)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ak-piracha "Code") |
| [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") | [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") |
| [<img src="https://avatars.githubusercontent.com/u/111287779?v=4" width="110px;"/><br /><sub>NojoudAlshehri</sub>](https://github.com/NojoudAlshehri)<br />[💻](https://github.com/snipe/snipe-it/commits?author=NojoudAlshehri "Code") | [<img src="https://avatars.githubusercontent.com/u/54367449?v=4" width="110px;"/><br /><sub>Stefan Stidl</sub>](https://github.com/stefanstidlffg)<br />[💻](https://github.com/snipe/snipe-it/commits?author=stefanstidlffg "Code") | [<img src="https://avatars.githubusercontent.com/u/87803479?v=4" width="110px;"/><br /><sub>Quentin Aymard</sub>](https://github.com/qay21)<br />[💻](https://github.com/snipe/snipe-it/commits?author=qay21 "Code") | [<img src="https://avatars.githubusercontent.com/u/5396871?v=4" width="110px;"/><br /><sub>Grant Le Roux</sub>](https://github.com/cram42)<br />[💻](https://github.com/snipe/snipe-it/commits?author=cram42 "Code") | [<img src="https://avatars.githubusercontent.com/u/58479551?v=4" width="110px;"/><br /><sub>Bogdan</sub>](http://@singrity)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Singrity "Code") | [<img src="https://avatars.githubusercontent.com/u/3483684?v=4" width="110px;"/><br /><sub>mmanjos</sub>](https://github.com/mmanjos)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mmanjos "Code") | [<img src="https://avatars.githubusercontent.com/u/7429229?v=4" width="110px;"/><br /><sub>Abdelaziz Faki</sub>](https://azooz2014.github.io/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Azooz2014 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "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

@ -91,6 +91,7 @@ class LdapSync extends Command
Log::debug('Importing users from specified location OU: \"'.$search_base.'\".');
}
}
else if ($this->option('base_dn') != '') {
$search_base = $this->option('base_dn');
Log::debug('Importing users from specified base DN: \"'.$search_base.'\".');

View file

@ -18,31 +18,36 @@ class AccessoryCheckoutController extends Controller
* Return the form to checkout an Accessory to a user.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $accessoryId
* @param int $id
* @return View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create($accessoryId)
public function create($id)
{
// Check if the accessory exists
if (is_null($accessory = Accessory::withCount('users as users_count')->find($accessoryId))) {
// Redirect to the accessory management page with error
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
}
// Make sure there is at least one available to checkout
if ($accessory->numRemaining() <= 0){
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkout.unavailable'));
}
if ($accessory->category) {
if ($accessory = Accessory::withCount('users as users_count')->find($id)) {
$this->authorize('checkout', $accessory);
// Get the dropdown of users and then pass it to the checkout view
return view('accessories/checkout', compact('accessory'));
if ($accessory->category) {
// Make sure there is at least one available to checkout
if ($accessory->numRemaining() <= 0){
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.checkout.unavailable'));
}
// Return the checkout view
return view('accessories/checkout', compact('accessory'));
}
// Invalid category
return redirect()->route('accessories.edit', ['accessory' => $accessory->id])
->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.accessory')]));
}
return redirect()->back()->with('error', 'The category type for this accessory is not valid. Edit the accessory and select a valid accessory category.');
// Not found
return redirect()->route('accessories.index')->with('error', trans('admin/accessories/message.not_found'));
}
/**

View file

@ -3,7 +3,6 @@
namespace App\Http\Controllers\Api;
use App\Events\CheckoutableCheckedIn;
use App\Http\Requests\StoreAssetRequest;
use Illuminate\Support\Facades\Gate;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
@ -534,8 +533,10 @@ class AssetsController extends Controller
* @since [v4.0]
* @return \Illuminate\Http\JsonResponse
*/
public function store(StoreAssetRequest $request)
public function store(ImageUploadRequest $request)
{
$this->authorize('create', Asset::class);
$asset = new Asset();
$asset->model()->associate(AssetModel::find((int) $request->get('model_id')));
@ -545,8 +546,7 @@ class AssetsController extends Controller
$asset->model_id = $request->get('model_id');
$asset->order_number = $request->get('order_number');
$asset->notes = $request->get('notes');
$asset->asset_tag = $request->get('asset_tag', Asset::autoincrement_asset()); //yup, problem :/
// NO IT IS NOT!!! This is never firing; we SHOW the asset_tag you're going to get, so it *will* be filled in!
$asset->asset_tag = $request->get('asset_tag', Asset::autoincrement_asset());
$asset->user_id = Auth::id();
$asset->archived = '0';
$asset->physical = '1';
@ -754,34 +754,24 @@ class AssetsController extends Controller
*/
public function restore(Request $request, $assetId = null)
{
// Get asset information
$asset = Asset::withTrashed()->find($assetId);
$this->authorize('delete', $asset);
if (isset($asset->id)) {
if ($asset = Asset::withTrashed()->find($assetId)) {
$this->authorize('delete', $asset);
if ($asset->deleted_at=='') {
$message = 'Asset was not deleted. No data was changed.';
} else {
$message = trans('admin/hardware/message.restore.success');
// Restore the asset
Asset::withTrashed()->where('id', $assetId)->restore();
$logaction = new Actionlog();
$logaction->item_type = Asset::class;
$logaction->item_id = $asset->id;
$logaction->created_at = date("Y-m-d H:i:s");
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restored');
if ($asset->deleted_at == '') {
return response()->json(Helper::formatStandardApiResponse('error', trans('general.not_deleted', ['item_type' => trans('general.asset')])), 200);
}
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset, $request), $message));
if ($asset->restore()) {
return response()->json(Helper::formatStandardApiResponse('success', trans('admin/hardware/message.restore.success')), 200);
}
// Check validation to make sure we're not restoring an asset with the same asset tag (or unique attribute) as an existing asset
return response()->json(Helper::formatStandardApiResponse('error', trans('general.could_not_restore', ['item_type' => trans('general.asset'), 'error' => $asset->getErrors()->first()])), 200);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200);
}
/**

View file

@ -263,9 +263,14 @@ class ConsumablesController extends Controller
// Make sure there is at least one available to checkout
if ($consumable->numRemaining() <= 0) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/consumables/message.checkout.unavailable')));
\Log::debug('No enough remaining');
}
// Make sure there is a valid category
if (!$consumable->category){
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.invalid_item_category_single', ['type' => trans('general.consumable')])));
}
// Check if the user exists - @TODO: this should probably be handled via validation, not here??
if (!$user = User::find($request->input('assigned_to'))) {
// Return error message

View file

@ -6,9 +6,11 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\ManufacturersTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Models\Actionlog;
use App\Models\Manufacturer;
use Illuminate\Http\Request;
use App\Http\Requests\ImageUploadRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
class ManufacturersController extends Controller
@ -159,6 +161,44 @@ class ManufacturersController extends Controller
}
/**
* Restore a given Manufacturer (mark as un-deleted)
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.3.4]
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function restore($id)
{
$this->authorize('delete', Manufacturer::class);
if ($manufacturer = Manufacturer::withTrashed()->find($id)) {
if ($manufacturer->deleted_at == '') {
return response()->json(Helper::formatStandardApiResponse('error', trans('general.not_deleted', ['item_type' => trans('general.manufacturer')])), 200);
}
if ($manufacturer->restore()) {
$logaction = new Actionlog();
$logaction->item_type = Manufacturer::class;
$logaction->item_id = $manufacturer->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restore');
return response()->json(Helper::formatStandardApiResponse('success', trans('admin/manufacturers/message.restore.success')), 200);
}
// Check validation to make sure we're not restoring an item with the same unique attributes as a non-deleted one
return response()->json(Helper::formatStandardApiResponse('error', trans('general.could_not_restore', ['item_type' => trans('general.manufacturer'), 'error' => $manufacturer->getErrors()->first()])), 200);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/manufacturers/message.does_not_exist')));
}
/**
* Gets a paginated collection for the select2 menus
*

View file

@ -11,6 +11,7 @@ use App\Http\Transformers\ConsumablesTransformer;
use App\Http\Transformers\LicensesTransformer;
use App\Http\Transformers\SelectlistTransformer;
use App\Http\Transformers\UsersTransformer;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\License;
@ -691,17 +692,31 @@ class UsersController extends Controller
*/
public function restore($userId = null)
{
// Get asset information
$user = User::withTrashed()->find($userId);
$this->authorize('delete', $user);
if (isset($user->id)) {
// Restore the user
User::withTrashed()->where('id', $userId)->restore();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.restored')));
if ($user = User::withTrashed()->find($userId)) {
$this->authorize('delete', $user);
if ($user->deleted_at == '') {
return response()->json(Helper::formatStandardApiResponse('error', trans('general.not_deleted', ['item_type' => trans('general.user')])), 200);
}
if ($user->restore()) {
$logaction = new Actionlog();
$logaction->item_type = User::class;
$logaction->item_id = $user->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restore');
return response()->json(Helper::formatStandardApiResponse('success', trans('admin/users/message.restore.success')), 200);
}
// Check validation to make sure we're not restoring a user with the same username as an existing user
return response()->json(Helper::formatStandardApiResponse('error', trans('general.could_not_restore', ['item_type' => trans('general.user'), 'error' => $user->getErrors()->first()])), 200);
}
$id = $userId;
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))), 200);
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);
}
}

View file

@ -4,7 +4,10 @@ namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Input;
@ -209,7 +212,7 @@ class AssetModelsController extends Controller
$this->authorize('delete', AssetModel::class);
// Check if the model exists
if (is_null($model = AssetModel::find($modelId))) {
return redirect()->route('models.index')->with('error', trans('admin/models/message.not_found'));
return redirect()->route('models.index')->with('error', trans('admin/models/message.does_not_exist'));
}
if ($model->assets()->count() > 0) {
@ -237,22 +240,42 @@ class AssetModelsController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $modelId
* @param int $id
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function getRestore($modelId = null)
public function getRestore($id)
{
$this->authorize('create', AssetModel::class);
// Get user information
$model = AssetModel::withTrashed()->find($modelId);
if (isset($model->id)) {
$model->restore();
if ($model = AssetModel::withTrashed()->find($id)) {
return redirect()->route('models.index')->with('success', trans('admin/models/message.restore.success'));
if ($model->deleted_at == '') {
return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.asset_model')]));
}
if ($model->restore()) {
$logaction = new Actionlog();
$logaction->item_type = User::class;
$logaction->item_id = $model->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restore');
// Redirect them to the deleted page if there are more, otherwise the section index
$deleted_models = AssetModel::onlyTrashed()->count();
if ($deleted_models > 0) {
return redirect()->back()->with('success', trans('admin/models/message.restore.success'));
}
return redirect()->route('models.index')->with('success', trans('admin/models/message.restore.success'));
}
// Check validation
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.asset_model'), 'error' => $model->getErrors()->first()]));
}
return redirect()->back()->with('error', trans('admin/models/message.not_found'));
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
}

View file

@ -6,6 +6,7 @@ use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Manufacturer;
use Illuminate\Support\Facades\Log;
use App\Models\Asset;
use App\Models\AssetModel;
@ -204,8 +205,9 @@ class AssetsController extends Controller
}
if ($success) {
\Log::debug(e($asset->asset_tag));
return redirect()->route('hardware.index')
->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset->id), 'id', 'tag' => $asset->asset_tag]));
->with('success-unescaped', trans('admin/hardware/message.create.success_linked', ['link' => route('hardware.show', $asset->id), 'id', 'tag' => e($asset->asset_tag)]));
}
@ -794,21 +796,24 @@ class AssetsController extends Controller
*/
public function getRestore($assetId = null)
{
// Get asset information
$asset = Asset::withTrashed()->find($assetId);
$this->authorize('delete', $asset);
if (isset($asset->id)) {
// Restore the asset
Asset::withTrashed()->where('id', $assetId)->restore();
if ($asset = Asset::withTrashed()->find($assetId)) {
$this->authorize('delete', $asset);
$logaction = new Actionlog();
$logaction->item_type = Asset::class;
$logaction->item_id = $asset->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restored');
if ($asset->deleted_at == '') {
return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.asset')]));
}
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success'));
if ($asset->restore()) {
// Redirect them to the deleted page if there are more, otherwise the section index
$deleted_assets = Asset::onlyTrashed()->count();
if ($deleted_assets > 0) {
return redirect()->back()->with('success', trans('admin/hardware/message.restore.success'));
}
return redirect()->route('hardware.index')->with('success', trans('admin/hardware/message.restore.success'));
}
// Check validation to make sure we're not restoring an asset with the same asset tag (or unique attribute) as an existing asset
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.asset'), 'error' => $asset->getErrors()->first()]));
}
return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.does_not_exist'));

View file

@ -56,7 +56,6 @@ class LoginController extends Controller
parent::__construct();
$this->middleware('guest', ['except' => ['logout', 'postTwoFactorAuth', 'getTwoFactorAuth', 'getTwoFactorEnroll']]);
Session::put('backUrl', \URL::previous());
// $this->ldap = $ldap;
$this->saml = $saml;
}
@ -82,7 +81,6 @@ class LoginController extends Controller
}
if (Setting::getSettings()->login_common_disabled == '1') {
\Log::debug('login_common_disabled is set to 1 - return a 403');
return view('errors.403');
}
@ -123,7 +121,7 @@ class LoginController extends Controller
if ($user = Auth::user()) {
$user->last_login = \Carbon::now();
$user->save();
$user->saveQuietly();
}
} catch (\Exception $e) {
@ -199,7 +197,7 @@ class LoginController extends Controller
$user->email = $ldap_attr['email'];
$user->first_name = $ldap_attr['firstname'];
$user->last_name = $ldap_attr['lastname']; //FIXME (or TODO?) - do we need to map additional fields that we now support? E.g. country, phone, etc.
$user->save();
$user->saveQuietly();
} // End if(!user)
return $user;
}
@ -319,7 +317,7 @@ class LoginController extends Controller
if ($user = Auth::user()) {
$user->last_login = \Carbon::now();
$user->activated = 1;
$user->save();
$user->saveQuietly();
}
// Redirect to the users page
return redirect()->intended()->with('success', trans('auth/message.signin.success'));
@ -371,7 +369,7 @@ class LoginController extends Controller
[-2, -2, -2, -2]
);
$user->save(); // make sure to save *AFTER* displaying the barcode, or else we might save a two_factor_secret that we never actually displayed to the user if the barcode fails
$user->saveQuietly(); // make sure to save *AFTER* displaying the barcode, or else we might save a two_factor_secret that we never actually displayed to the user if the barcode fails
return view('auth.two_factor_enroll')->with('barcode_obj', $barcode_obj);
}
@ -426,7 +424,7 @@ class LoginController extends Controller
if (Google2FA::verifyKey($user->two_factor_secret, $secret)) {
$user->two_factor_enrolled = 1;
$user->save();
$user->saveQuietly();
$request->session()->put('2fa_authed', $user->id);
return redirect()->route('home')->with('success', 'You are logged in!');

View file

@ -20,25 +20,38 @@ class ComponentCheckoutController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @see ComponentCheckoutController::store() method that stores the data.
* @since [v3.0]
* @param int $componentId
* @param int $id
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create($componentId)
public function create($id)
{
// Check if the component exists
if (is_null($component = Component::find($componentId))) {
// Redirect to the component management page with error
return redirect()->route('components.index')->with('error', trans('admin/components/message.not_found'));
}
$this->authorize('checkout', $component);
// Make sure there is at least one available to checkout
if ($component->numRemaining() <= 0){
return redirect()->route('components.index')->with('error', trans('admin/components/message.checkout.unavailable'));
if ($component = Component::find($id)) {
$this->authorize('checkout', $component);
// Make sure the category is valid
if ($component->category) {
// Make sure there is at least one available to checkout
if ($component->numRemaining() <= 0){
return redirect()->route('components.index')
->with('error', trans('admin/components/message.checkout.unavailable'));
}
// Return the checkout view
return view('components/checkout', compact('component'));
}
// Invalid category
return redirect()->route('components.edit', ['component' => $component->id])
->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.component')]));
}
return view('components/checkout', compact('component'));
// Not found
return redirect()->route('components.index')->with('error', trans('admin/components/message.not_found'));
}
/**

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Consumables;
use App\Events\CheckoutableCheckedOut;
use App\Http\Controllers\Controller;
use App\Models\Accessory;
use App\Models\Consumable;
use App\Models\User;
use Illuminate\Http\Request;
@ -18,25 +19,38 @@ class ConsumableCheckoutController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @see ConsumableCheckoutController::store() method that stores the data.
* @since [v1.0]
* @param int $consumableId
* @param int $id
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create($consumableId)
public function create($id)
{
if (is_null($consumable = Consumable::with('users')->find($consumableId))) {
return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.does_not_exist'));
if ($consumable = Consumable::with('users')->find($id)) {
$this->authorize('checkout', $consumable);
// Make sure the category is valid
if ($consumable->category) {
// Make sure there is at least one available to checkout
if ($consumable->numRemaining() <= 0){
return redirect()->route('consumables.index')
->with('error', trans('admin/consumables/message.checkout.unavailable'));
}
// Return the checkout view
return view('consumables/checkout', compact('consumable'));
}
// Invalid category
return redirect()->route('consumables.edit', ['consumable' => $consumable->id])
->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.consumable')]));
}
// Make sure there is at least one available to checkout
if ($consumable->numRemaining() <= 0){
return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.checkout.unavailable'));
}
// Not found
return redirect()->route('consumables.index')->with('error', trans('admin/consumables/message.does_not_exist'));
$this->authorize('checkout', $consumable);
return view('consumables/checkout', compact('consumable'));
}
/**

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Licenses;
use App\Events\CheckoutableCheckedOut;
use App\Http\Controllers\Controller;
use App\Http\Requests\LicenseCheckoutRequest;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\License;
use App\Models\LicenseSeat;
@ -21,23 +22,35 @@ class LicenseCheckoutController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param $licenseId
* @param $id
* @return \Illuminate\Contracts\View\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create($licenseId)
public function create($id)
{
// Check that the license is valid
if ($license = License::find($licenseId)) {
if ($license = License::find($id)) {
$this->authorize('checkout', $license);
// If the license is valid, check that there is an available seat
if ($license->avail_seats_count < 1) {
return redirect()->route('licenses.index')->with('error', 'There are no available seats for this license');
if ($license->category) {
// Make sure there is at least one available to checkout
if ($license->availCount()->count() < 1){
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.checkout.not_enough_seats'));
}
// Return the checkout view
return view('licenses/checkout', compact('license'));
}
return view('licenses/checkout', compact('license'));
// Invalid category
return redirect()->route('licenses.edit', ['license' => $license->id])
->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.license')]));
}
// Not found
return redirect()->route('licenses.index')->with('error', trans('admin/licenses/message.not_found'));

View file

@ -2,8 +2,12 @@
namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Manufacturer;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
@ -218,22 +222,37 @@ class ManufacturersController extends Controller
* @return Redirect
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function restore($manufacturers_id)
public function restore($id)
{
$this->authorize('create', Manufacturer::class);
$manufacturer = Manufacturer::onlyTrashed()->where('id', $manufacturers_id)->first();
$this->authorize('delete', Manufacturer::class);
if ($manufacturer) {
if ($manufacturer = Manufacturer::withTrashed()->find($id)) {
if ($manufacturer->deleted_at == '') {
return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.manufacturer')]));
}
// Not sure why this is necessary - it shouldn't fail validation here, but it fails without this, so....
$manufacturer->setValidating(false);
if ($manufacturer->restore()) {
$logaction = new Actionlog();
$logaction->item_type = Manufacturer::class;
$logaction->item_id = $manufacturer->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restore');
// Redirect them to the deleted page if there are more, otherwise the section index
$deleted_manufacturers = Manufacturer::onlyTrashed()->count();
if ($deleted_manufacturers > 0) {
return redirect()->back()->with('success', trans('admin/manufacturers/message.success.restored'));
}
return redirect()->route('manufacturers.index')->with('success', trans('admin/manufacturers/message.restore.success'));
}
return redirect()->back()->with('error', 'Could not restore.');
// Check validation to make sure we're not restoring an asset with the same asset tag (or unique attribute) as an existing asset
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.manufacturer'), 'error' => $manufacturer->getErrors()->first()]));
}
return redirect()->back()->with('error', trans('admin/manufacturers/message.does_not_exist'));
return redirect()->route('manufacturers.index')->with('error', trans('admin/manufacturers/message.does_not_exist'));
}
}

View file

@ -134,6 +134,7 @@ class ProfileController extends Controller
];
$validator = \Validator::make($request->all(), $rules);
$validator->after(function ($validator) use ($request, $user) {
if (! Hash::check($request->input('current_password'), $user->password)) {
$validator->errors()->add('current_password', trans('validation.custom.hashed_pass'));
@ -159,12 +160,14 @@ class ProfileController extends Controller
});
if (! $validator->fails()) {
$user->password = Hash::make($request->input('password'));
$user->save();
$user->password = Hash::make($request->input('password'));
// We have to use saveQuietly here because for some reason this method was calling the User Oserver twice :(
$user->saveQuietly();
// Log the user out of other devices
Auth::logoutOtherDevices($request->input('password'));
return redirect()->route('account.password.index')->with('success', 'Password updated!');
return redirect()->route('account')->with('success', trans('passwords.password_change'));
}
return redirect()->back()->withInput()->withErrors($validator);

View file

@ -7,10 +7,10 @@ use App\Http\Controllers\Controller;
use App\Http\Controllers\UserNotFoundException;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\SaveUserRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Group;
use App\Models\Ldap;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
@ -385,18 +385,35 @@ class UsersController extends Controller
*/
public function getRestore($id = null)
{
$this->authorize('update', User::class);
// Get user information
if (! User::onlyTrashed()->find($id)) {
return redirect()->route('users.index')->with('error', trans('admin/users/messages.user_not_found'));
if ($user = User::withTrashed()->find($id)) {
$this->authorize('delete', $user);
if ($user->deleted_at == '') {
return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.user')]));
}
if ($user->restore()) {
$logaction = new Actionlog();
$logaction->item_type = User::class;
$logaction->item_id = $user->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('restore');
// Redirect them to the deleted page if there are more, otherwise the section index
$deleted_users = User::onlyTrashed()->count();
if ($deleted_users > 0) {
return redirect()->back()->with('success', trans('admin/users/message.success.restored'));
}
return redirect()->route('users.index')->with('success', trans('admin/users/message.success.restored'));
}
// Check validation to make sure we're not restoring a user with the same username as an existing user
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.user'), 'error' => $user->getErrors()->first()]));
}
// Restore the user
if (User::withTrashed()->where('id', $id)->restore()) {
return redirect()->route('users.index')->with('success', trans('admin/users/message.success.restored'));
}
return redirect()->route('users.index')->with('error', 'User could not be restored.');
return redirect()->route('users.index')->with('error', trans('admin/users/message.does_not_exist'));
}
/**

View file

@ -1,25 +0,0 @@
<?php
namespace App\Http\Traits;
use App\Models\Setting;
trait UniqueSerialTrait
{
/**
* Prepare a unique_ids rule, adding a model identifier if required.
*
* @param array $parameters
* @param string $field
*
* @return string
*/
protected function prepareUniqueSerialRule($parameters, $field)
{
if ($settings = Setting::getSettings()) {
if ($settings->unique_serial == '1') {
return 'unique_undeleted:'.$this->table.','.$this->getKey();
}
}
}
}

View file

@ -73,7 +73,7 @@ class AssetModelsTransformer
$permissions_array['available_actions'] = [
'update' => (Gate::allows('update', AssetModel::class) && ($assetmodel->deleted_at == '')),
'delete' => (Gate::allows('delete', AssetModel::class) && ($assetmodel->assets_count == 0)),
'delete' => $assetmodel->isDeletable(),
'clone' => (Gate::allows('create', AssetModel::class) && ($assetmodel->deleted_at == '')),
'restore' => (Gate::allows('create', AssetModel::class) && ($assetmodel->deleted_at != '')),
];

View file

@ -147,7 +147,7 @@ class AssetsTransformer
'clone' => Gate::allows('create', Asset::class) ? true : false,
'restore' => ($asset->deleted_at!='' && Gate::allows('create', Asset::class)) ? true : false,
'update' => ($asset->deleted_at=='' && Gate::allows('update', Asset::class)) ? true : false,
'delete' => ($asset->deleted_at=='' && $asset->assigned_to =='' && Gate::allows('delete', Asset::class)) ? true : false,
'delete' => ($asset->deleted_at=='' && $asset->assigned_to =='' && Gate::allows('delete', Asset::class) && ($asset->deleted_at == '')) ? true : false,
];

View file

@ -79,7 +79,7 @@ class UsersTransformer
$permissions_array['available_actions'] = [
'update' => (Gate::allows('update', User::class) && ($user->deleted_at == '')),
'delete' => (Gate::allows('delete', User::class) && ($user->assets_count == 0) && ($user->licenses_count == 0) && ($user->accessories_count == 0)),
'delete' => $user->isDeletable(),
'clone' => (Gate::allows('create', User::class) && ($user->deleted_at == '')),
'restore' => (Gate::allows('create', User::class) && ($user->deleted_at != '')),
];

View file

@ -61,7 +61,7 @@ class UserImporter extends ItemImporter
$this->item['activated'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'activated'))) == 1) ? '1' : 0;
$this->item['employee_num'] = trim($this->findCsvMatch($row, 'employee_num'));
$this->item['department_id'] = trim($this->createOrFetchDepartment(trim($this->findCsvMatch($row, 'department'))));
$this->item['manager_id'] = $this->fetchManager($this->findCsvMatch($row, 'manager_first_name'), $this->findCsvMatch($row, 'manager_last_name'));
$this->item['manager_id'] = $this->fetchManager(trim($this->findCsvMatch($row, 'manager_first_name')), trim($this->findCsvMatch($row, 'manager_last_name')));
$this->item['remote'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'remote'))) == 1 ) ? '1' : 0;
$this->item['vip'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'vip'))) ==1 ) ? '1' : 0;
$this->item['autoassign_licenses'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'autoassign_licenses'))) ==1 ) ? '1' : 0;

View file

@ -69,7 +69,6 @@ class LogListener
$logaction->item()->associate($event->acceptance->checkoutable->license);
}
\Log::debug('New onCheckoutAccepted Listener fired. logaction: '.print_r($logaction, true));
$logaction->save();
}

View file

@ -6,7 +6,6 @@ use App\Events\AssetCheckedOut;
use App\Events\CheckoutableCheckedOut;
use App\Exceptions\CheckoutNotAllowed;
use App\Helpers\Helper;
use App\Http\Traits\UniqueSerialTrait;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Traits\Acceptable;
use App\Models\Traits\Searchable;
@ -32,7 +31,7 @@ class Asset extends Depreciable
protected $presenter = \App\Presenters\AssetPresenter::class;
use CompanyableTrait;
use HasFactory, Loggable, Requestable, Presentable, SoftDeletes, ValidatingTrait, UniqueUndeletedTrait, UniqueSerialTrait;
use HasFactory, Loggable, Requestable, Presentable, SoftDeletes, ValidatingTrait, UniqueUndeletedTrait;
public const LOCATION = 'location';
public const ASSET = 'asset';
@ -100,9 +99,9 @@ class Asset extends Depreciable
'expected_checkin' => 'date|nullable',
'location_id' => 'exists:locations,id|nullable',
'rtd_location_id' => 'exists:locations,id|nullable',
'asset_tag' => 'required|min:1|max:255|unique_undeleted:assets,asset_tag',
'asset_tag' => 'required|min:1|max:255|unique_undeleted:assets,asset_tag|not_array',
'purchase_date' => 'date|date_format:Y-m-d|nullable',
'serial' => 'unique_serial|nullable',
'serial' => 'unique_undeleted:assets,serial|nullable',
'purchase_cost' => 'numeric|nullable|gte:0',
'supplier_id' => 'exists:suppliers,id|nullable',
'asset_eol_date' => 'date|nullable',
@ -110,6 +109,7 @@ class Asset extends Depreciable
'byod' => 'boolean',
];
/**
* The attributes that are mass assignable.
*

View file

@ -6,6 +6,7 @@ use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
@ -188,6 +189,21 @@ class AssetModel extends SnipeModel
return false;
}
/**
* Checks if the model is deletable
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.3.4]
* @return bool
*/
public function isDeletable()
{
return Gate::allows('delete', $this)
&& ($this->assets_count == 0)
&& ($this->deleted_at == '');
}
/**
* Get uploads for this model
*

View file

@ -100,7 +100,8 @@ class Category extends SnipeModel
{
return Gate::allows('delete', $this)
&& ($this->itemCount() == 0);
&& ($this->itemCount() == 0)
&& ($this->deleted_at == '');
}
/**
@ -247,6 +248,26 @@ class Category extends SnipeModel
}
}
/**
* -----------------------------------------------
* BEGIN MUTATORS
* -----------------------------------------------
**/
/**
* This sets the checkin_value to a boolean 0 or 1. This accounts for forms or API calls that
* explicitly pass the checkin_email field but it has a null or empty value.
*
* This will also correctly parse a 1/0 if "true"/"false" is passed.
*
* @param $value
* @return void
*/
public function setCheckinEmailAttribute($value)
{
$this->attributes['checkin_email'] = (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
/**
* -----------------------------------------------
* BEGIN QUERY SCOPES

View file

@ -77,7 +77,8 @@ class Manufacturer extends SnipeModel
&& ($this->assets()->count() === 0)
&& ($this->licenses()->count() === 0)
&& ($this->consumables()->count() === 0)
&& ($this->accessories()->count() === 0);
&& ($this->accessories()->count() === 0)
&& ($this->deleted_at == '');
}
public function assets()

View file

@ -129,8 +129,20 @@ class SnipeSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig
'preferredLanguage' => AttributeMapping::eloquent('locale'), // Section 5.3.5 of [RFC7231]
'locale' => null, // see RFC5646
'timezone' => null, // see RFC6557
'active' => AttributeMapping::eloquent('activated'),
'active' => (new AttributeMapping())->setAdd(
function ($value, &$object) {
$object->activated = $value;
}
)->setReplace(
function ($value, &$object) {
$object->activated = $value;
}
)->setRead(
// this works as specified.
function (&$object) {
return (bool)$object->activated;
}
),
'password' => AttributeMapping::eloquent('password')->disableRead(),
// Multi-Valued Attributes

View file

@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Gate;
use Laravel\Passport\HasApiTokens;
use Watson\Validating\ValidatingTrait;
@ -201,6 +202,23 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $this->checkPermissionSection('superuser');
}
/**
* Checks if the user is deletable
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.3.4]
* @return bool
*/
public function isDeletable()
{
return Gate::allows('delete', $this)
&& ($this->assets()->count() === 0)
&& ($this->licenses()->count() === 0)
&& ($this->consumables()->count() === 0)
&& ($this->accessories()->count() === 0)
&& ($this->deleted_at == '');
}
/**
* Establishes the user -> company relationship

View file

@ -22,6 +22,13 @@ class AssetObserver
$attributesOriginal = $asset->getRawOriginal();
$same_checkout_counter = false;
$same_checkin_counter = false;
$restoring_or_deleting = false;
// This is a gross hack to prevent the double logging when restoring an asset
if (array_key_exists('deleted_at', $attributes) && array_key_exists('deleted_at', $attributesOriginal)){
$restoring_or_deleting = (($attributes['deleted_at'] != $attributesOriginal['deleted_at']));
}
if (array_key_exists('checkout_counter', $attributes) && array_key_exists('checkout_counter', $attributesOriginal)){
$same_checkout_counter = (($attributes['checkout_counter'] == $attributesOriginal['checkout_counter']));
@ -33,10 +40,10 @@ class AssetObserver
// If the asset isn't being checked out or audited, log the update.
// (Those other actions already create log entries.)
if (($attributes['assigned_to'] == $attributesOriginal['assigned_to'])
if (($attributes['assigned_to'] == $attributesOriginal['assigned_to'])
&& ($same_checkout_counter) && ($same_checkin_counter)
&& ((isset( $attributes['next_audit_date']) ? $attributes['next_audit_date'] : null) == (isset($attributesOriginal['next_audit_date']) ? $attributesOriginal['next_audit_date']: null))
&& ($attributes['last_checkout'] == $attributesOriginal['last_checkout']))
&& ($attributes['last_checkout'] == $attributesOriginal['last_checkout']) && (!$restoring_or_deleting))
{
$changed = [];
@ -121,6 +128,22 @@ class AssetObserver
$logAction->logaction('delete');
}
/**
* Listen to the Asset deleting event.
*
* @param Asset $asset
* @return void
*/
public function restoring(Asset $asset)
{
$logAction = new Actionlog();
$logAction->item_type = Asset::class;
$logAction->item_id = $asset->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id();
$logAction->logaction('restore');
}
/**
* Executes every time an asset is saved.
*

View file

@ -0,0 +1,149 @@
<?php
namespace App\Observers;
use App\Models\Actionlog;
use App\Models\User;
use Auth;
class UserObserver
{
/**
* Listen to the User updating event. This fires automatically every time an existing asset is saved.
*
* @param User $user
* @return void
*/
public function updating(User $user)
{
// ONLY allow these fields to be stored
$allowed_fields = [
'email',
'activated',
'first_name',
'last_name',
'website',
'country',
'gravatar',
'location_id',
'phone',
'jobtitle',
'manager_id',
'employee_num',
'username',
'notes',
'company_id',
'ldap_import',
'locale',
'two_factor_enrolled',
'two_factor_optin',
'department_id',
'address',
'address2',
'city',
'state',
'zip',
'remote',
'start_date',
'end_date',
'autoassign_licenses',
'vip',
'password'
];
$changed = [];
foreach ($user->getRawOriginal() as $key => $value) {
// Make sure the info is in the allow fields array
if (in_array($key, $allowed_fields)) {
// Check and see if the value changed
if ($user->getRawOriginal()[$key] != $user->getAttributes()[$key]) {
$changed[$key]['old'] = $user->getRawOriginal()[$key];
$changed[$key]['new'] = $user->getAttributes()[$key];
// Do not store the hashed password in changes
if ($key == 'password') {
$changed['password']['old'] = '*************';
$changed['password']['new'] = '*************';
}
}
}
}
if (count($changed) > 0) {
$logAction = new Actionlog();
$logAction->item_type = User::class;
$logAction->item_id = $user->id;
$logAction->target_type = User::class; // can we instead say $logAction->item = $asset ?
$logAction->target_id = $user->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id();
$logAction->log_meta = json_encode($changed);
$logAction->logaction('update');
}
}
/**
* Listen to the User created event, and increment
* the next_auto_tag_base value in the settings table when i
* a new asset is created.
*
* @param User $user
* @return void
*/
public function created(User $user)
{
$logAction = new Actionlog();
$logAction->item_type = User::class; // can we instead say $logAction->item = $asset ?
$logAction->item_id = $user->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id();
$logAction->logaction('create');
}
/**
* Listen to the User deleting event.
*
* @param User $user
* @return void
*/
public function deleting(User $user)
{
$logAction = new Actionlog();
$logAction->item_type = User::class;
$logAction->item_id = $user->id;
$logAction->target_type = User::class; // can we instead say $logAction->item = $asset ?
$logAction->target_id = $user->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id();
$logAction->logaction('delete');
}
/**
* Listen to the User deleting event.
*
* @param User $user
* @return void
*/
public function restoring(User $user)
{
$logAction = new Actionlog();
$logAction->item_type = User::class;
$logAction->item_id = $user->id;
$logAction->target_type = User::class; // can we instead say $logAction->item = $asset ?
$logAction->target_id = $user->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->user_id = Auth::id();
$logAction->logaction('restore');
}
}

View file

@ -38,22 +38,63 @@ class ActionlogPresenter extends Presenter
public function icon()
{
$itemicon = 'fas fa-paperclip';
// User related icons
if ($this->itemType() == 'user') {
if ($this->itemType() == 'asset') {
return 'fas fa-barcode';
} elseif ($this->itemType() == 'accessory') {
return 'far fa-keyboard';
} elseif ($this->itemType() == 'consumable') {
return 'fas fa-tint';
} elseif ($this->itemType() == 'license') {
return 'far fa-save';
} elseif ($this->itemType() == 'component') {
return 'far fa-hdd';
} elseif ($this->itemType() == 'user') {
return 'fa-solid fa-people-arrows';
if ($this->actionType()=='create new') {
return 'fa-solid fa-user-plus';
}
if ($this->actionType()=='merged') {
return 'fa-solid fa-people-arrows';
}
if ($this->actionType()=='delete') {
return 'fa-solid fa-user-minus';
}
if ($this->actionType()=='delete') {
return 'fa-solid fa-user-minus';
}
if ($this->actionType()=='update') {
return 'fa-solid fa-user-pen';
}
return 'fa-solid fa-user';
}
// Everything else
if ($this->actionType()=='create new') {
return 'fa-solid fa-plus';
}
if ($this->actionType()=='delete') {
return 'fa-solid fa-user-xmark';
}
if ($this->actionType()=='update') {
return 'fa-solid fa-pen';
}
if ($this->actionType()=='restore') {
return 'fa-solid fa-trash-arrow-up';
}
if ($this->actionType()=='upload') {
return 'fas fa-paperclip';
}
if ($this->actionType()=='checkout') {
return 'fa-solid fa-rotate-left';
}
if ($this->actionType()=='checkin from') {
return 'fa-solid fa-rotate-right';
}
return 'fa-solid fa-rotate-right';
}
public function actionType()

View file

@ -7,10 +7,12 @@ use App\Models\Asset;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\License;
use App\Models\User;
use App\Models\Setting;
use App\Models\SnipeSCIMConfig;
use App\Observers\AccessoryObserver;
use App\Observers\AssetObserver;
use App\Observers\UserObserver;
use App\Observers\ComponentObserver;
use App\Observers\ConsumableObserver;
use App\Observers\LicenseObserver;
@ -58,6 +60,7 @@ class AppServiceProvider extends ServiceProvider
Schema::defaultStringLength(191);
Asset::observe(AssetObserver::class);
User::observe(UserObserver::class);
Accessory::observe(AccessoryObserver::class);
Component::observe(ComponentObserver::class);
Consumable::observe(ConsumableObserver::class);

View file

@ -3,6 +3,7 @@
namespace App\Providers;
use App\Models\Department;
use App\Models\Setting;
use DB;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rule;
@ -45,32 +46,87 @@ class ValidationServiceProvider extends ServiceProvider
return $validator->passes();
});
// Unique only if undeleted
// This works around the use case where multiple deleted items have the same unique attribute.
// (I think this is a bug in Laravel's validator?)
// $parameters is the rule parameters, like `unique_undeleted:users,id` - $parameters[0] is users, $parameters[1] is id
// the UniqueUndeletedTrait prefills these so you can just use `unique_undeleted` in your rules (but this would only work directly in the model)
/**
* Unique only if undeleted.
*
* This works around the use case where multiple deleted items have the same unique attribute.
* (I think this is a bug in Laravel's validator?)
*
* $attribute is the FIELDNAME you're checking against
* $value is the VALUE of the item you're checking against the existing values in the fieldname
* $parameters[0] is the TABLE NAME you're querying
* $parameters[1] is the ID of the item you're querying - this makes it work on saving, checkout, etc,
* since it defaults to 0 if there is no item created yet (new item), but populates the ID if editing
*
* The UniqueUndeletedTrait prefills these parameters, so you can just use
* `unique_undeleted:table,fieldname` in your rules out of the box
*/
Validator::extend('unique_undeleted', function ($attribute, $value, $parameters, $validator) {
if (count($parameters)) {
$count = DB::table($parameters[0])->select('id')->where($attribute, '=', $value)->whereNull('deleted_at')->where('id', '!=', $parameters[1])->count();
// This is a bit of a shim, but serial doesn't have any other rules around it other than that it's nullable
if (($parameters[0]=='assets') && ($attribute == 'serial') && (Setting::getSettings()->unique_serial != '1')) {
return true;
}
$count = DB::table($parameters[0])
->select('id')
->where($attribute, '=', $value)
->whereNull('deleted_at')
->where('id', '!=', $parameters[1])->count();
return $count < 1;
}
});
/**
* Unique if undeleted for two columns
*
* Same as unique_undeleted but taking the combination of two columns as unique constrain.
* This uses the Validator::replacer('two_column_unique_undeleted') below for nicer translations.
*
* $parameters[0] - the name of the first table we're looking at
* $parameters[1] - the ID (this will be 0 on new creations)
* $parameters[2] - the name of the second table we're looking at
* $parameters[3] - the value that the request is passing for the second table we're
* checking for uniqueness across
*
*/
Validator::extend('two_column_unique_undeleted', function ($attribute, $value, $parameters, $validator) {
if (count($parameters)) {
$count = DB::table($parameters[0])
->select('id')->where($attribute, '=', $value)
->whereNull('deleted_at')
->where('id', '!=', $parameters[1])
->where($parameters[2], $parameters[3])->count();
return $count < 1;
}
});
// Unique if undeleted for two columns
// Same as unique_undeleted but taking the combination of two columns as unique constrain.
Validator::extend('two_column_unique_undeleted', function ($attribute, $value, $parameters, $validator) {
if (count($parameters)) {
$count = DB::table($parameters[0])
->select('id')->where($attribute, '=', $value)
->whereNull('deleted_at')
->where('id', '!=', $parameters[1])
->where($parameters[2], $parameters[3])->count();
return $count < 1;
}
});
/**
* This is the validator replace static method that allows us to pass the $parameters of the table names
* into the translation string in validation.two_column_unique_undeleted for two_column_unique_undeleted
* validation messages.
*
* This is invoked automatically by Validator::extend('two_column_unique_undeleted') above and
* produces a translation like: "The name value must be unique across categories and category type."
*
* The $parameters passed coincide with the ones the two_column_unique_undeleted custom validator above
* uses, so $parameter[0] is the first table and so $parameter[2] is the second table.
*/
Validator::replacer('two_column_unique_undeleted', function($message, $attribute, $rule, $parameters) {
$message = str_replace(':table1', $parameters[0], $message);
$message = str_replace(':table2', $parameters[2], $message);
// Change underscores to spaces for a friendlier display
$message = str_replace('_', ' ', $message);
return $message;
});
// Prevent circular references
//

View file

@ -1,10 +1,10 @@
<?php
return array (
'app_version' => 'v6.2.3',
'full_app_version' => 'v6.2.3 - build 11936-gb47e734b3',
'build_version' => '11936',
'app_version' => 'v6.2.4-pre',
'full_app_version' => 'v6.2.4-pre - build 12090-g776b16d37',
'build_version' => '12090',
'prerelease_version' => '',
'hash_version' => 'gb47e734b3',
'full_hash' => 'v6.2.3-175-gb47e734b3',
'hash_version' => 'g776b16d37',
'full_hash' => 'v6.2.4-pre-329-g776b16d37',
'branch' => 'develop',
);

4292
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -25,11 +25,11 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.2",
"acorn": "^8.9.0",
"acorn": "^8.11.2",
"acorn-import-assertions": "^1.9.0",
"admin-lte": "^2.4.18",
"ajv": "^6.12.6",
"alpinejs": "^3.10.5",
"alpinejs": "^3.13.2",
"blueimp-file-upload": "^9.34.0",
"bootstrap": "^3.4.1",
"bootstrap-colorpicker": "^2.5.3",
@ -45,7 +45,7 @@
"jquery-ui": "^1.13.2",
"jquery-ui-bundle": "^1.12.1",
"jquery.iframe-transport": "^1.0.0",
"jspdf-autotable": "^3.5.30",
"jspdf-autotable": "^3.7.1",
"less": "^4.2.0",
"less-loader": "^6.0",
"list.js": "^1.5.0",
@ -56,6 +56,6 @@
"tableexport.jquery.plugin": "1.28.0",
"tether": "^1.4.0",
"vue-resource": "^1.5.2",
"webpack": "^5.88.2"
"webpack": "^5.89.0"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -14,7 +14,7 @@
"/css/dist/skins/skin-red-dark.css": "/css/dist/skins/skin-red-dark.css?id=7f0eb9e355b36b41c61c3af3b4d41143",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=f4d95ad9d0944587549e35b6929b4b04",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=1f33ca3d860461c1127ec465ab3ebb6b",
"/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=44f9320d0739f419c9246f7f39395b02",
"/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=0ed42b67f9b02a74815e885bfd9e3f66",
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=f0fbbb0ac729ea092578fb05ca615460",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=b9a74ec0cd68f83e7480d5ae39919beb",
@ -33,9 +33,9 @@
"/js/build/vendor.js": "/js/build/vendor.js?id=ede02ee2aad89fe10c64a87f6c76838a",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=1f678160a05960c3087fb8263168ff41",
"/js/dist/all.js": "/js/dist/all.js?id=b9dbb598e7c52f20fa595893cfa1199d",
"/js/dist/all-defer.js": "/js/dist/all-defer.js?id=55eff1c09b1f966c0c4f16d898f89c86",
"/js/dist/all-defer.js": "/js/dist/all-defer.js?id=7f9a130eda6916eaa32a0a57e81918f3",
"/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=b48f4d8af0e1ca5621c161e93951109f",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=44f9320d0739f419c9246f7f39395b02",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=0ed42b67f9b02a74815e885bfd9e3f66",
"/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=1f33ca3d860461c1127ec465ab3ebb6b",
"/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=f4d95ad9d0944587549e35b6929b4b04",
"/css/dist/skins/skin-blue.min.css": "/css/dist/skins/skin-blue.min.css?id=392cc93cfc0be0349bab9697669dd091",

View file

@ -361,7 +361,7 @@ input[type=text], input[type=search] {
background-color: var(--back-sub);
}
.table-striped>tbody>tr:nth-of-type(even){
background-color: var(--back-sub-alt);
background-color: var(--back-sub);
}
#webui>div>div>div>div>div>table>tbody>tr>td>a>i.fa, .box-body, .box-footer, .box-header {
color: var(--text-main);

View file

@ -9,6 +9,7 @@ return array(
'assoc_users' => 'This license is currently checked out to a user and cannot be deleted. Please check the license in first, and then try deleting again. ',
'select_asset_or_person' => 'You must select an asset or a user, but not both.',
'not_found' => 'License not found',
'seats_available' => ':seat_count seats available',
'create' => array(
@ -41,7 +42,8 @@ return array(
'checkout' => array(
'error' => 'There was an issue checking out the license. Please try again.',
'success' => 'The license was checked out successfully'
'success' => 'The license was checked out successfully',
'not_enough_seats' => 'Not enough license seats available for checkout',
),
'checkin' => array(

View file

@ -72,6 +72,8 @@ return [
'consumable' => 'Consumable',
'consumables' => 'Consumables',
'country' => 'Country',
'could_not_restore' => 'Error restoring :item_type: :error',
'not_deleted' => 'The :item_type is not deleted so it cannot be restored',
'create' => 'Create New',
'created' => 'Item Created',
'created_asset' => 'created asset',
@ -353,7 +355,8 @@ return [
'synchronize' => 'Synchronize',
'sync_results' => 'Synchronization Results',
'license_serial' => 'Serial/Product Key',
'invalid_category' => 'Invalid category',
'invalid_category' => 'Invalid or missing category',
'invalid_item_category_single' => 'Invalid or missing :type category. Please update the category of this :type to include a valid category before checking out.',
'dashboard_info' => 'This is your dashboard. There are many like it, but this one is yours.',
'60_percent_warning' => '60% Complete (warning)',
'dashboard_empty' => 'It looks like you have not added anything yet, so we do not have anything awesome to display. Get started by adding some assets, accessories, consumables, or licenses now!',
@ -485,7 +488,7 @@ return [
],
'percent_complete' => '% complete',
'uploading' => 'Uploading... ',
'upload_error' => 'Error uploading file. Please check that there are no empty trailing rows.',
'upload_error' => 'Error uploading file. Please check that there are no empty rows and that no column names are duplicated.',
'copy_to_clipboard' => 'Copy to Clipboard',
'copied' => 'Copied!',

View file

@ -5,4 +5,5 @@ return [
'user' => 'If a matching user with a valid email address exists in our system, a password recovery email has been sent.',
'token' => 'This password reset token is invalid or expired, or does not match the username provided.',
'reset' => 'Your password has been reset!',
'password_change' => 'Your password has been updated!',
];

View file

@ -90,12 +90,14 @@ return [
],
'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid zone.',
'two_column_unique_undeleted' => 'The :attribute must be unique across :table1 and :table2. ',
'unique' => 'The :attribute has already been taken.',
'uploaded' => 'The :attribute failed to upload.',
'url' => 'The :attribute format is invalid.',
'unique_undeleted' => 'The :attribute must be unique.',
'non_circular' => 'The :attribute must not create a circular reference.',
'not_array' => 'The :attribute field cannot be an array.',
'unique_serial' => 'The :attribute must be unique.',
'disallow_same_pwd_as_user_fields' => 'Password cannot be the same as the username.',
'letters' => 'Password must contain at least one letter.',
'numbers' => 'Password must contain at least one number.',

View file

@ -10,15 +10,17 @@
@section('content')
@if ($acceptances = \App\Models\CheckoutAcceptance::forUser(Auth::user())->pending()->count())
<div class="col-md-12">
<div class="alert alert alert-warning fade in">
<i class="fas fa-exclamation-triangle faa-pulse animated"></i>
<div class="row">
<div class="col-md-12">
<div class="alert alert alert-warning fade in">
<i class="fas fa-exclamation-triangle faa-pulse animated"></i>
<strong>
<a href="{{ route('account.accept') }}" style="color: white;">
{{ trans('general.unaccepted_profile_warning', array('count' => $acceptances)) }}
</a>
</strong>
<strong>
<a href="{{ route('account.accept') }}" style="color: white;">
{{ trans('general.unaccepted_profile_warning', array('count' => $acceptances)) }}
</a>
</strong>
</div>
</div>
</div>
@endif
@ -388,7 +390,6 @@
data-show-columns="true"
data-show-export="true"
data-show-footer="true"
data-show-refresh="false"
data-sort-order="asc"
id="userAssets"
class="table table-striped snipe-table"
@ -405,6 +406,7 @@
<th class="col-md-2" data-switchable="true" data-visible="true">{{ trans('general.name') }}</th>
<th class="col-md-2" data-switchable="true" data-visible="true">{{ trans('admin/hardware/table.asset_model') }}</th>
<th class="col-md-3" data-switchable="true" data-visible="true">{{ trans('admin/hardware/table.serial') }}</th>
<th class="col-md-2" data-switchable="true" data-visible="false">{{ trans('admin/hardware/form.default_location') }}</th>
@can('self.view_purchase_cost')
<th class="col-md-6" data-footer-formatter="sumFormatter" data-fieldname="purchase_cost">{{ trans('general.purchase_cost') }}</th>
@endcan
@ -442,7 +444,7 @@
@endif
</td>
<td>{{ $asset->serial }}</td>
<td>{{ ($asset->defaultLoc) ? $asset->defaultLoc->name : '' }}</td>
@can('self.view_purchase_cost')
<td>
{!! Helper::formatCurrencyOutput($asset->purchase_cost) !!}

View file

@ -29,7 +29,7 @@
@can('update', $custom_fieldset)
<th class="col-md-1"><span class="sr-only">{{ trans('admin/custom_fields/general.reorder') }}</span></th>
@endcan
<th class="col-md-1">{{ trans('admin/custom_fields/general.order') }}</th>
<th class="col-md-1" style="display: none;">{{ trans('admin/custom_fields/general.order') }}</th>
<th class="col-md-3">{{ trans('admin/custom_fields/general.field_name') }}</th>
<th class="col-md-2">{{ trans('admin/custom_fields/general.field_format') }}</th>
<th class="col-md-2">{{ trans('admin/custom_fields/general.field_element') }}</th>
@ -51,7 +51,7 @@
</span>
</td>
@endcan
<td class="index">{{$field->pivot->order + 1}}</td>
<td class="index" style="display: none;">{{$field->pivot->order + 1}}</td> {{--this +1 needs to exist to keep the first row from reverting to 0 if you edit/delete fields in/from a fielset--}}
<td>{{$field->name}}</td>
<td>{{$field->format}}</td>
<td>{{$field->element}}</td>
@ -110,7 +110,7 @@
</label>
</div>
<div class="form-group col-md-2">
<div class="form-group col-md-2" style="display: none;">
{{ Form::text('order', $maxid, array('class' => 'form-control col-sm-1 col-md-1', 'style'=> 'width: 80px; padding-;right: 10px;', 'aria-label'=>'order', 'maxlength'=>'3', 'size'=>'3')) }}
<label for="order">{{ trans('admin/custom_fields/general.order') }}</label>

View file

@ -21,17 +21,25 @@
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title"> {{ $license->name }}</h2>
<h2 class="box-title"> {{ $license->name }} ({{ trans('admin/licenses/message.seats_available', ['seat_count' => $license->availCount()->count()]) }})</h2>
</div>
<div class="box-body">
<!-- Asset name -->
<div class="form-group">
<label class="col-sm-3 control-label">{{ trans('admin/hardware/form.name') }}</label>
<div class="col-md-6">
<div class="col-md-9">
<p class="form-control-static">{{ $license->name }}</p>
</div>
</div>
<!-- Category -->
<div class="form-group">
<label class="col-sm-3 control-label">{{ trans('general.category') }}</label>
<div class="col-md-9">
<p class="form-control-static">{{ $license->category->name }}</p>
</div>
</div>
<!-- Serial -->
<div class="form-group">
@ -57,8 +65,8 @@
<!-- Note -->
<div class="form-group {{ $errors->has('notes') ? 'error' : '' }}">
<label for="note" class="col-md-3 control-label">{{ trans('admin/hardware/form.notes') }}</label>
<div class="col-md-7">
<textarea class="col-md-6 form-control" id="notes" name="notes">{{ old('note') }}</textarea>
<div class="col-md-8">
<textarea class="col-md-6 form-control" id="notes" name="notes" style="width: 100%">{{ old('note') }}</textarea>
{!! $errors->first('note', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>

View file

@ -235,7 +235,7 @@
])
}}
</div>
@if ($activeFile->first_row)
@if (($activeFile->first_row) && (array_key_exists($index, $activeFile->first_row)))
<div class="col-md-5">
<p class="form-control-static">{{ str_limit($activeFile->first_row[$index], 50, '...') }}</p>
</div>

View file

@ -279,7 +279,11 @@
+ ' data-title="{{ trans('general.delete') }}" onClick="return false;">'
+ '<i class="fas fa-trash" aria-hidden="true"></i><span class="sr-only">{{ trans('general.delete') }}</span></a>&nbsp;';
} else {
actions += '<span data-tooltip="true" title="{{ trans('general.cannot_be_deleted') }}"><a class="btn btn-danger btn-sm delete-asset disabled" onClick="return false;"><i class="fas fa-trash"></i></a></span>&nbsp;';
// Do not show the delete button on things that are already deleted
if ((row.available_actions) && (row.available_actions.restore != true)) {
actions += '<span data-tooltip="true" title="{{ trans('general.cannot_be_deleted') }}"><a class="btn btn-danger btn-sm delete-asset disabled" onClick="return false;"><i class="fas fa-trash"></i></a></span>&nbsp;';
}
}

View file

@ -706,6 +706,13 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.manufacturers.selectlist');
Route::post('{id}/restore',
[
Api\ManufacturersController::class,
'restore'
]
)->name('api.manufacturers.restore');
});
Route::resource('manufacturers',
@ -742,6 +749,13 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.models.assets');
Route::post('{id}/restore',
[
Api\AssetModelsController::class,
'restore'
]
)->name('api.models.restore');
});
Route::resource('models',