From d14f43fbbe0e28a3a5b7f1d3c7c31cc6259540d6 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Mon, 18 Nov 2024 10:54:03 -0800 Subject: [PATCH 01/18] replace env with config variable for from address --- app/Mail/CheckinAccessoryMail.php | 2 +- app/Mail/CheckinAssetMail.php | 2 +- app/Mail/CheckinLicenseMail.php | 2 +- app/Mail/CheckoutAccessoryMail.php | 2 +- app/Mail/CheckoutAssetMail.php | 2 +- app/Mail/CheckoutConsumableMail.php | 2 +- app/Mail/CheckoutLicenseMail.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Mail/CheckinAccessoryMail.php b/app/Mail/CheckinAccessoryMail.php index fc8d1455f9..707be0c2b4 100644 --- a/app/Mail/CheckinAccessoryMail.php +++ b/app/Mail/CheckinAccessoryMail.php @@ -34,7 +34,7 @@ class CheckinAccessoryMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); + $from = new Address(config('mail.from.address','service@snipe-it.io')); return new Envelope( from: $from, diff --git a/app/Mail/CheckinAssetMail.php b/app/Mail/CheckinAssetMail.php index 5dd8394a5a..3b457c7f03 100644 --- a/app/Mail/CheckinAssetMail.php +++ b/app/Mail/CheckinAssetMail.php @@ -43,7 +43,7 @@ class CheckinAssetMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); + $from = new Address(config('mail.from.address','service@snipe-it.io')); return new Envelope( from: $from, diff --git a/app/Mail/CheckinLicenseMail.php b/app/Mail/CheckinLicenseMail.php index 8957f367ef..73e09eabf4 100644 --- a/app/Mail/CheckinLicenseMail.php +++ b/app/Mail/CheckinLicenseMail.php @@ -34,7 +34,7 @@ class CheckinLicenseMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); + $from = new Address(config('mail.from.address','service@snipe-it.io')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutAccessoryMail.php b/app/Mail/CheckoutAccessoryMail.php index f297c02754..6c6baeb633 100644 --- a/app/Mail/CheckoutAccessoryMail.php +++ b/app/Mail/CheckoutAccessoryMail.php @@ -37,7 +37,7 @@ class CheckoutAccessoryMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); + $from = new Address(config('mail.from.address','service@snipe-it.io')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutAssetMail.php b/app/Mail/CheckoutAssetMail.php index fa1290e92d..84c2fb2165 100644 --- a/app/Mail/CheckoutAssetMail.php +++ b/app/Mail/CheckoutAssetMail.php @@ -52,7 +52,7 @@ class CheckoutAssetMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(env('MAIL_FROM_ADDR', 'service@snipe-it.io')); + $from = new Address(config('mail.from.address', 'service@snipe-it.io')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutConsumableMail.php b/app/Mail/CheckoutConsumableMail.php index 18fe228258..aa9a4dc803 100644 --- a/app/Mail/CheckoutConsumableMail.php +++ b/app/Mail/CheckoutConsumableMail.php @@ -38,7 +38,7 @@ class CheckoutConsumableMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); + $from = new Address(config('mail.from.address','service@snipe-it.io')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutLicenseMail.php b/app/Mail/CheckoutLicenseMail.php index 7377ad340e..a7dffe4ea4 100644 --- a/app/Mail/CheckoutLicenseMail.php +++ b/app/Mail/CheckoutLicenseMail.php @@ -36,7 +36,7 @@ class CheckoutLicenseMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(env('MAIL_FROM_ADDR','service@snipe-it.io')); + $from = new Address(config('mail.from.address','service@snipe-it.io')); return new Envelope( from: $from, From f533cdc07aea557e53adcf36e2ab1df689fd18ba Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Mon, 18 Nov 2024 12:39:28 -0800 Subject: [PATCH 02/18] update default from address to alternate config variable --- app/Mail/CheckinAccessoryMail.php | 2 +- app/Mail/CheckinAssetMail.php | 2 +- app/Mail/CheckinLicenseMail.php | 2 +- app/Mail/CheckoutAccessoryMail.php | 2 +- app/Mail/CheckoutAssetMail.php | 2 +- app/Mail/CheckoutConsumableMail.php | 2 +- app/Mail/CheckoutLicenseMail.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/Mail/CheckinAccessoryMail.php b/app/Mail/CheckinAccessoryMail.php index 707be0c2b4..d573a4436f 100644 --- a/app/Mail/CheckinAccessoryMail.php +++ b/app/Mail/CheckinAccessoryMail.php @@ -34,7 +34,7 @@ class CheckinAccessoryMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address','service@snipe-it.io')); + $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); return new Envelope( from: $from, diff --git a/app/Mail/CheckinAssetMail.php b/app/Mail/CheckinAssetMail.php index 3b457c7f03..cf660b21ea 100644 --- a/app/Mail/CheckinAssetMail.php +++ b/app/Mail/CheckinAssetMail.php @@ -43,7 +43,7 @@ class CheckinAssetMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address','service@snipe-it.io')); + $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); return new Envelope( from: $from, diff --git a/app/Mail/CheckinLicenseMail.php b/app/Mail/CheckinLicenseMail.php index 73e09eabf4..709879ec1b 100644 --- a/app/Mail/CheckinLicenseMail.php +++ b/app/Mail/CheckinLicenseMail.php @@ -34,7 +34,7 @@ class CheckinLicenseMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address','service@snipe-it.io')); + $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutAccessoryMail.php b/app/Mail/CheckoutAccessoryMail.php index 6c6baeb633..1c3f53402d 100644 --- a/app/Mail/CheckoutAccessoryMail.php +++ b/app/Mail/CheckoutAccessoryMail.php @@ -37,7 +37,7 @@ class CheckoutAccessoryMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address','service@snipe-it.io')); + $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutAssetMail.php b/app/Mail/CheckoutAssetMail.php index 84c2fb2165..3a1480e8bd 100644 --- a/app/Mail/CheckoutAssetMail.php +++ b/app/Mail/CheckoutAssetMail.php @@ -52,7 +52,7 @@ class CheckoutAssetMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address', 'service@snipe-it.io')); + $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutConsumableMail.php b/app/Mail/CheckoutConsumableMail.php index aa9a4dc803..833b562699 100644 --- a/app/Mail/CheckoutConsumableMail.php +++ b/app/Mail/CheckoutConsumableMail.php @@ -38,7 +38,7 @@ class CheckoutConsumableMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address','service@snipe-it.io')); + $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutLicenseMail.php b/app/Mail/CheckoutLicenseMail.php index a7dffe4ea4..691169d8ac 100644 --- a/app/Mail/CheckoutLicenseMail.php +++ b/app/Mail/CheckoutLicenseMail.php @@ -36,7 +36,7 @@ class CheckoutLicenseMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address','service@snipe-it.io')); + $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); return new Envelope( from: $from, From 889aff43c2657566611628480e1553c0add8e540 Mon Sep 17 00:00:00 2001 From: NebelKreis Date: Tue, 19 Nov 2024 14:13:05 +0100 Subject: [PATCH 03/18] Feature: Added API endpoint for generating asset labels --- app/Http/Controllers/Api/AssetsController.php | 340 ++++++++++++------ 1 file changed, 222 insertions(+), 118 deletions(-) diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index d4a103be37..11725286e1 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -33,6 +33,8 @@ use Illuminate\Http\Request; use App\Http\Requests\ImageUploadRequest; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Route; +use App\View\Label; +use Illuminate\Support\Facades\Storage; /** @@ -80,10 +82,10 @@ class AssetsController extends Controller $this->authorize('reports.view'); } else { $transformer = 'App\Http\Transformers\AssetsTransformer'; - $this->authorize('index', Asset::class); + $this->authorize('index', Asset::class); } - - + + $settings = Setting::getSettings(); $allowed_columns = [ @@ -126,8 +128,19 @@ class AssetsController extends Controller } $assets = Asset::select('assets.*') - ->with('location', 'assetstatus', 'company', 'defaultLoc','assignedTo', 'adminuser','model.depreciation', - 'model.category', 'model.manufacturer', 'model.fieldset','supplier'); //it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users. + ->with( + 'location', + 'assetstatus', + 'company', + 'defaultLoc', + 'assignedTo', + 'adminuser', + 'model.depreciation', + 'model.category', + 'model.manufacturer', + 'model.fieldset', + 'supplier' + ); //it might be tempting to add 'assetlog' here, but don't. It blows up update-heavy users. if ($filter_non_deprecable_assets) { @@ -159,8 +172,8 @@ class AssetsController extends Controller * Handle due and overdue audits and checkin dates */ switch ($action) { - // Audit (singular) is left over from earlier legacy APIs - case 'audits' : + // Audit (singular) is left over from earlier legacy APIs + case 'audits': switch ($upcoming_status) { case 'due': $assets->DueForAudit($settings); @@ -187,7 +200,7 @@ class AssetsController extends Controller break; } break; - } + } /** * End handling due and overdue audits and checkin dates @@ -265,7 +278,6 @@ class AssetsController extends Controller $join->on('status_alias.id', '=', 'assets.status_id'); }); } - } @@ -345,7 +357,7 @@ class AssetsController extends Controller $column_sort = in_array($sort_override, $allowed_columns) ? $sort_override : 'assets.created_at'; $order = $request->input('order') === 'asc' ? 'asc' : 'desc'; - + switch ($sort_override) { case 'model': $assets->OrderModels($order); @@ -399,7 +411,6 @@ class AssetsController extends Controller } else { $assets->orderBy($sort_override, $order); } - } else { $assets->orderBy($column_sort, $order); } @@ -413,11 +424,11 @@ class AssetsController extends Controller $total = $assets->count(); $assets = $assets->skip($offset)->take($limit)->get(); - + /** * Include additional associated relationships - */ + */ if ($request->input('components')) { $assets->loadMissing(['components' => function ($query) { $query->orderBy('created_at', 'desc'); @@ -441,7 +452,7 @@ class AssetsController extends Controller * @since [v4.2.1] * @author [A. Gianotto] [] */ - public function showByTag(Request $request, $tag) : JsonResponse | array + public function showByTag(Request $request, $tag): JsonResponse | array { $this->authorize('index', Asset::class); $assets = Asset::where('asset_tag', $tag)->with('assetstatus')->with('assignedTo'); @@ -463,12 +474,10 @@ class AssetsController extends Controller } else { return (new AssetsTransformer)->transformAssets($assets, $assets->count()); } - } // If there are 0 results, return the "no such asset" response return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); - } /** @@ -479,7 +488,7 @@ class AssetsController extends Controller * @since [v4.2.1] * @return \Illuminate\Http\JsonResponse */ - public function showBySerial(Request $request, $serial) : JsonResponse | array + public function showBySerial(Request $request, $serial): JsonResponse | array { $this->authorize('index', Asset::class); $assets = Asset::where('serial', $serial)->with('assetstatus')->with('assignedTo'); @@ -488,14 +497,13 @@ class AssetsController extends Controller if ($request->input('deleted', 'false') == 'true') { $assets = $assets->withTrashed(); } - + if (($assets = $assets->get()) && ($assets->count()) > 0) { - return (new AssetsTransformer)->transformAssets($assets, $assets->count()); + return (new AssetsTransformer)->transformAssets($assets, $assets->count()); } // If there are 0 results, return the "no such asset" response return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); - } /** @@ -506,20 +514,20 @@ class AssetsController extends Controller * @since [v4.0] * @return \Illuminate\Http\JsonResponse */ - public function show(Request $request, $id) : JsonResponse | array + public function show(Request $request, $id): JsonResponse | array { if ($asset = Asset::with('assetstatus') ->with('assignedTo')->withTrashed() - ->withCount('checkins as checkins_count', 'checkouts as checkouts_count', 'userRequests as user_requests_count')->find($id)) { + ->withCount('checkins as checkins_count', 'checkouts as checkouts_count', 'userRequests as user_requests_count')->find($id) + ) { $this->authorize('view', $asset); - return (new AssetsTransformer)->transformAsset($asset, $request->input('components') ); + return (new AssetsTransformer)->transformAsset($asset, $request->input('components')); } return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); - } - public function licenses(Request $request, $id) : array + public function licenses(Request $request, $id): array { $this->authorize('view', Asset::class); $this->authorize('view', License::class); @@ -527,7 +535,7 @@ class AssetsController extends Controller $licenses = $asset->licenses()->get(); return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count()); - } + } /** @@ -537,7 +545,7 @@ class AssetsController extends Controller * @since [v4.0.16] * @see \App\Http\Transformers\SelectlistTransformer */ - public function selectlist(Request $request) : array + public function selectlist(Request $request): array { $assets = Asset::select([ @@ -548,7 +556,7 @@ class AssetsController extends Controller 'assets.assigned_to', 'assets.assigned_type', 'assets.status_id', - ])->with('model', 'assetstatus', 'assignedTo')->NotArchived(); + ])->with('model', 'assetstatus', 'assignedTo')->NotArchived(); if ($request->filled('assetStatusType') && $request->input('assetStatusType') === 'RTD') { $assets = $assets->RTD(); @@ -570,12 +578,12 @@ class AssetsController extends Controller $asset->use_text = $asset->present()->fullName; if (($asset->checkedOutToUser()) && ($asset->assigned)) { - $asset->use_text .= ' → '.$asset->assigned->getFullNameAttribute(); + $asset->use_text .= ' → ' . $asset->assigned->getFullNameAttribute(); } if ($asset->assetstatus->getStatuslabelType() == 'pending') { - $asset->use_text .= '('.$asset->assetstatus->getStatuslabelType().')'; + $asset->use_text .= '(' . $asset->assetstatus->getStatuslabelType() . ')'; } $asset->use_image = ($asset->getImageUrl()) ? $asset->getImageUrl() : null; @@ -601,12 +609,12 @@ class AssetsController extends Controller $asset->created_by = auth()->id(); /** - * this is here just legacy reasons. Api\AssetController - * used image_source once to allow encoded image uploads. - */ + * this is here just legacy reasons. Api\AssetController + * used image_source once to allow encoded image uploads. + */ if ($request->has('image_source')) { $request->offsetSet('image', $request->offsetGet('image_source')); - } + } $asset = $request->handleImages($asset); @@ -623,9 +631,9 @@ class AssetsController extends Controller // If input value is null, use custom field's default value if ($field_val == null) { - Log::debug('Field value for '.$field->db_column.' is null'); + Log::debug('Field value for ' . $field->db_column . ' is null'); $field_val = $field->defaultValue($request->get('model_id')); - Log::debug('Use the default fieldset value of '.$field->defaultValue($request->get('model_id'))); + Log::debug('Use the default fieldset value of ' . $field->defaultValue($request->get('model_id'))); } // if the field is set to encrypted, make sure we encrypt the value @@ -643,7 +651,7 @@ class AssetsController extends Controller } } if ($field->element == 'checkbox') { - if(is_array($field_val)) { + if (is_array($field_val)) { $field_val = implode(',', $field_val); } } @@ -702,59 +710,59 @@ class AssetsController extends Controller } /** - * this is here just legacy reasons. Api\AssetController - * used image_source once to allow encoded image uploads. - */ + * this is here just legacy reasons. Api\AssetController + * used image_source once to allow encoded image uploads. + */ if ($request->has('image_source')) { $request->offsetSet('image', $request->offsetGet('image_source')); } $asset = $request->handleImages($asset); $model = $asset->model; - - // Update custom fields - $problems_updating_encrypted_custom_fields = false; - if (($model) && (isset($model->fieldset))) { - foreach ($model->fieldset->fields as $field) { - $field_val = $request->input($field->db_column, null); - if ($request->has($field->db_column)) { - if ($field->element == 'checkbox') { - if(is_array($field_val)) { - $field_val = implode(',', $field_val); - } + // Update custom fields + $problems_updating_encrypted_custom_fields = false; + if (($model) && (isset($model->fieldset))) { + foreach ($model->fieldset->fields as $field) { + $field_val = $request->input($field->db_column, null); + + if ($request->has($field->db_column)) { + if ($field->element == 'checkbox') { + if (is_array($field_val)) { + $field_val = implode(',', $field_val); } - if ($field->field_encrypted == '1') { - if (Gate::allows('assets.view.encrypted_custom_fields')) { - $field_val = Crypt::encrypt($field_val); - } else { - $problems_updating_encrypted_custom_fields = true; - continue; - } - } - $asset->{$field->db_column} = $field_val; } + if ($field->field_encrypted == '1') { + if (Gate::allows('assets.view.encrypted_custom_fields')) { + $field_val = Crypt::encrypt($field_val); + } else { + $problems_updating_encrypted_custom_fields = true; + continue; + } + } + $asset->{$field->db_column} = $field_val; } } - if ($asset->save()) { - if (($request->filled('assigned_user')) && ($target = User::find($request->get('assigned_user')))) { - $location = $target->location_id; - } elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->get('assigned_asset')))) { - $location = $target->location_id; + } + if ($asset->save()) { + if (($request->filled('assigned_user')) && ($target = User::find($request->get('assigned_user')))) { + $location = $target->location_id; + } elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->get('assigned_asset')))) { + $location = $target->location_id; - Asset::where('assigned_type', \App\Models\Asset::class)->where('assigned_to', $asset->id) - ->update(['location_id' => $target->location_id]); - } elseif (($request->filled('assigned_location')) && ($target = Location::find($request->get('assigned_location')))) { - $location = $target->id; - } + Asset::where('assigned_type', \App\Models\Asset::class)->where('assigned_to', $asset->id) + ->update(['location_id' => $target->location_id]); + } elseif (($request->filled('assigned_location')) && ($target = Location::find($request->get('assigned_location')))) { + $location = $target->id; + } - if (isset($target)) { - $asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', e($request->get('name')), $location); - } + if (isset($target)) { + $asset->checkOut($target, auth()->user(), date('Y-m-d H:i:s'), '', 'Checked out on asset update', e($request->get('name')), $location); + } - if ($asset->image) { - $asset->image = $asset->getImageUrl(); - } + if ($asset->image) { + $asset->image = $asset->getImageUrl(); + } if ($problems_updating_encrypted_custom_fields) { return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning'))); @@ -773,7 +781,7 @@ class AssetsController extends Controller * @param int $assetId * @since [v4.0] */ - public function destroy($id) : JsonResponse + public function destroy($id): JsonResponse { $this->authorize('delete', Asset::class); @@ -799,7 +807,7 @@ class AssetsController extends Controller return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); } - + /** * Restore a soft-deleted asset. @@ -808,7 +816,7 @@ class AssetsController extends Controller * @param int $assetId * @since [v5.1.18] */ - public function restore(Request $request, $assetId = null) : JsonResponse + public function restore(Request $request, $assetId = null): JsonResponse { if ($asset = Asset::withTrashed()->find($assetId)) { @@ -827,7 +835,6 @@ class AssetsController extends Controller } return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); - } /** @@ -837,7 +844,7 @@ class AssetsController extends Controller * @param string $tag * @since [v6.0.5] */ - public function checkoutByTag(AssetCheckoutRequest $request, $tag) : JsonResponse + public function checkoutByTag(AssetCheckoutRequest $request, $tag): JsonResponse { if ($asset = Asset::where('asset_tag', $tag)->first()) { return $this->checkout($request, $asset->id); @@ -852,13 +859,13 @@ class AssetsController extends Controller * @param int $assetId * @since [v4.0] */ - public function checkout(AssetCheckoutRequest $request, $asset_id) : JsonResponse + public function checkout(AssetCheckoutRequest $request, $asset_id): JsonResponse { $this->authorize('checkout', Asset::class); $asset = Asset::findOrFail($asset_id); if (! $asset->availableForCheckout()) { - return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkout.not_available'))); + return response()->json(Helper::formatStandardApiResponse('error', ['asset' => e($asset->asset_tag)], trans('admin/hardware/message.checkout.not_available'))); } $this->authorize('checkout', $asset); @@ -875,14 +882,12 @@ class AssetsController extends Controller $asset->location_id = ($target) ? $target->id : ''; $error_payload['target_id'] = $request->input('assigned_location'); $error_payload['target_type'] = 'location'; - } elseif (request('checkout_to_type') == 'asset') { $target = Asset::where('id', '!=', $asset_id)->find(request('assigned_asset')); // Override with the asset's location_id if it has one $asset->location_id = (($target) && (isset($target->location_id))) ? $target->location_id : ''; $error_payload['target_id'] = $request->input('assigned_asset'); $error_payload['target_type'] = 'asset'; - } elseif (request('checkout_to_type') == 'user') { // Fetch the target and set the asset's new location_id $target = User::find(request('assigned_user')); @@ -896,7 +901,7 @@ class AssetsController extends Controller } if (! isset($target)) { - return response()->json(Helper::formatStandardApiResponse('error', $error_payload, 'Checkout target for asset '.e($asset->asset_tag).' is invalid - '.$error_payload['target_type'].' does not exist.')); + return response()->json(Helper::formatStandardApiResponse('error', $error_payload, 'Checkout target for asset ' . e($asset->asset_tag) . ' is invalid - ' . $error_payload['target_type'] . ' does not exist.')); } $checkout_at = request('checkout_at', date('Y-m-d H:i:s')); @@ -910,15 +915,15 @@ class AssetsController extends Controller // TODO: Follow up here. WTF. Commented out for now. -// if ((isset($target->rtd_location_id)) && ($asset->rtd_location_id!='')) { -// $asset->location_id = $target->rtd_location_id; -// } + // if ((isset($target->rtd_location_id)) && ($asset->rtd_location_id!='')) { + // $asset->location_id = $target->rtd_location_id; + // } 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'))); + return response()->json(Helper::formatStandardApiResponse('success', ['asset' => e($asset->asset_tag)], trans('admin/hardware/message.checkout.success'))); } - return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkout.error'))); + return response()->json(Helper::formatStandardApiResponse('error', ['asset' => e($asset->asset_tag)], trans('admin/hardware/message.checkout.error'))); } @@ -929,7 +934,7 @@ class AssetsController extends Controller * @param int $assetId * @since [v4.0] */ - public function checkin(Request $request, $asset_id) : JsonResponse + public function checkin(Request $request, $asset_id): JsonResponse { $asset = Asset::with('model')->findOrFail($asset_id); $this->authorize('checkin', $asset); @@ -937,7 +942,7 @@ class AssetsController extends Controller $target = $asset->assignedTo; if (is_null($target)) { return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset_tag'=> e($asset->asset_tag), + 'asset_tag' => e($asset->asset_tag), 'model' => e($asset->model->name), 'model_number' => e($asset->model->model_number) ], trans('admin/hardware/message.checkin.already_checked_in'))); @@ -960,7 +965,7 @@ class AssetsController extends Controller if ($request->filled('location_id')) { $asset->location_id = $request->input('location_id'); - if ($request->input('update_default_location')){ + if ($request->input('update_default_location')) { $asset->rtd_location_id = $request->input('location_id'); } } @@ -968,8 +973,8 @@ class AssetsController extends Controller if ($request->filled('status_id')) { $asset->status_id = $request->input('status_id'); } - - $checkin_at = $request->filled('checkin_at') ? $request->input('checkin_at').' '. date('H:i:s') : date('Y-m-d H:i:s'); + + $checkin_at = $request->filled('checkin_at') ? $request->input('checkin_at') . ' ' . date('H:i:s') : date('Y-m-d H:i:s'); $originalValues = $asset->getRawOriginal(); if (($request->filled('checkin_at')) && ($request->get('checkin_at') != date('Y-m-d'))) { @@ -987,7 +992,8 @@ class AssetsController extends Controller [Asset::class], function (Builder $query) use ($asset) { $query->where('id', $asset->id); - }) + } + ) ->get() ->map(function ($acceptance) { $acceptance->delete(); @@ -997,13 +1003,13 @@ class AssetsController extends Controller event(new CheckoutableCheckedIn($asset, $target, auth()->user(), $request->input('note'), $checkin_at, $originalValues)); return response()->json(Helper::formatStandardApiResponse('success', [ - 'asset_tag'=> e($asset->asset_tag), + 'asset_tag' => e($asset->asset_tag), 'model' => e($asset->model->name), 'model_number' => e($asset->model->model_number) ], trans('admin/hardware/message.checkin.success'))); } - return response()->json(Helper::formatStandardApiResponse('error', ['asset'=> e($asset->asset_tag)], trans('admin/hardware/message.checkin.error'))); + return response()->json(Helper::formatStandardApiResponse('error', ['asset' => e($asset->asset_tag)], trans('admin/hardware/message.checkin.error'))); } /** @@ -1012,7 +1018,7 @@ class AssetsController extends Controller * @author [A. Janes] [] * @since [v6.0] */ - public function checkinByTag(Request $request, $tag = null) : JsonResponse + public function checkinByTag(Request $request, $tag = null): JsonResponse { $this->authorize('checkin', Asset::class); if (null == $tag && null !== ($request->input('asset_tag'))) { @@ -1025,8 +1031,8 @@ class AssetsController extends Controller } return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset'=> e($tag) - ], 'Asset with tag '.e($tag).' not found')); + 'asset' => e($tag) + ], 'Asset with tag ' . e($tag) . ' not found')); } @@ -1037,7 +1043,7 @@ class AssetsController extends Controller * @param int $id * @since [v4.0] */ - public function audit(Request $request) : JsonResponse + public function audit(Request $request): JsonResponse { $this->authorize('audit', Asset::class); @@ -1048,8 +1054,8 @@ class AssetsController extends Controller // No tag passed - return an error if (!$request->filled('asset_tag')) { return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset_tag'=> '', - 'error'=> trans('admin/hardware/message.no_tag'), + 'asset_tag' => '', + 'error' => trans('admin/hardware/message.no_tag'), ], trans('admin/hardware/message.no_tag')), 200); } @@ -1097,28 +1103,25 @@ class AssetsController extends Controller $asset->logAudit(request('note'), request('location_id')); return response()->json(Helper::formatStandardApiResponse('success', [ - 'asset_tag'=> e($asset->asset_tag), - 'note'=> e($request->input('note')), + 'asset_tag' => e($asset->asset_tag), + 'note' => e($request->input('note')), 'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date), ], trans('admin/hardware/message.audit.success'))); } // Asset failed validation or was not able to be saved return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset_tag'=> e($asset->asset_tag), - 'error'=> $asset->getErrors()->first(), + 'asset_tag' => e($asset->asset_tag), + 'error' => $asset->getErrors()->first(), ], trans('admin/hardware/message.audit.error', ['error' => $asset->getErrors()->first()])), 200); - } // No matching asset for the asset tag that was passed. return response()->json(Helper::formatStandardApiResponse('error', [ - 'asset_tag'=> e($request->input('asset_tag')), - 'error'=> trans('admin/hardware/message.audit.error'), + 'asset_tag' => e($request->input('asset_tag')), + 'error' => trans('admin/hardware/message.audit.error'), ], trans('admin/hardware/message.audit.error', ['error' => trans('admin/hardware/message.does_not_exist')])), 200); - - } @@ -1129,7 +1132,7 @@ class AssetsController extends Controller * @author [A. Gianotto] [] * @since [v4.0] */ - public function requestable(Request $request) : JsonResponse | array + public function requestable(Request $request): JsonResponse | array { $this->authorize('viewRequestable', Asset::class); @@ -1150,8 +1153,18 @@ class AssetsController extends Controller } $assets = Asset::select('assets.*') - ->with('location', 'assetstatus', 'assetlog', 'company','assignedTo', - 'model.category', 'model.manufacturer', 'model.fieldset', 'supplier', 'requests'); + ->with( + 'location', + 'assetstatus', + 'assetlog', + 'company', + 'assignedTo', + 'model.category', + 'model.manufacturer', + 'model.fieldset', + 'supplier', + 'requests' + ); @@ -1159,7 +1172,7 @@ class AssetsController extends Controller if ($request->filled('search')) { $assets->TextSearch($request->input('search')); } - + // Search custom fields by column name foreach ($all_custom_fields as $field) { if ($request->filled($field->db_column_name())) { @@ -1200,4 +1213,95 @@ class AssetsController extends Controller return (new AssetsTransformer)->transformRequestedAssets($assets, $total); } + + /** + * Generate asset labels by tag + * + * @author [Nebelkreis] [https://github.com/NebelKreis] + * + * @param Request $request Contains asset_tags array of asset tags to generate labels for + * @return JsonResponse Returns base64 encoded PDF on success, error message on failure + */ + public function getLabels(Request $request): JsonResponse + { + try { + $this->authorize('view', Asset::class); + + // Validate that asset tags were provided in the request + if (!$request->filled('asset_tags')) { + return response()->json(Helper::formatStandardApiResponse('error', null, + 'No assets selected.'), 400); + } + + // Convert asset tags from request into collection and fetch matching assets + $asset_tags = collect($request->input('asset_tags')); + $assets = Asset::whereIn('asset_tag', $asset_tags)->get(); + + // Return error if no assets were found for the provided tags + if ($assets->isEmpty()) { + return response()->json(Helper::formatStandardApiResponse('error', null, + 'Asset does not exist.'), 404); + } + + try { + $settings = Setting::getSettings(); + + if (!$settings) { + throw new \Exception('Settings could not be loaded'); + } + + // Check if logo file exists in storage and disable logo if not found + // This prevents errors when trying to include a non-existent logo in the PDF + $original_logo = $settings->label_logo; + if ($original_logo && !Storage::disk('public')->exists('/' . $original_logo)) { + $settings->label_logo = null; + } + + $label = new Label(); + + if (!$label) { + throw new \Exception('Label object could not be created'); + } + + // Configure label with assets and settings + // bulkedit=false and count=0 are default values for label generation + $label = $label->with('assets', $assets) + ->with('settings', $settings) + ->with('bulkedit', false) + ->with('count', 0); + + // Generate PDF using callback function + // The callback captures the PDF content in $pdf_content variable + $pdf_content = ''; + $label->render(function($pdf) use (&$pdf_content) { + $pdf_content = $pdf->Output('', 'S'); + return $pdf; + }); + + // Verify PDF was generated successfully + if (empty($pdf_content)) { + throw new \Exception('PDF content is empty'); + } + + $encoded_content = base64_encode($pdf_content); + + return response()->json(Helper::formatStandardApiResponse('success', [ + 'pdf' => $encoded_content + ], 'Labels were successfully generated.')); + + } catch (\Exception $e) { + return response()->json(Helper::formatStandardApiResponse('error', [ + 'error_message' => $e->getMessage(), + 'error_line' => $e->getLine(), + 'error_file' => $e->getFile() + ], 'Error while generating labels.'), 500); + } + } catch (\Exception $e) { + return response()->json(Helper::formatStandardApiResponse('error', [ + 'error_message' => $e->getMessage(), + 'error_line' => $e->getLine(), + 'error_file' => $e->getFile() + ], $e->getMessage()), 500); + } + } } From 66d6b01307d5a10d208552bed12156e6fb458b03 Mon Sep 17 00:00:00 2001 From: NebelKreis Date: Tue, 19 Nov 2024 14:16:06 +0100 Subject: [PATCH 04/18] Feature: Added translations for label generation API endpoint --- app/Http/Controllers/Api/AssetsController.php | 8 ++++---- resources/lang/en-US/admin/hardware/message.php | 13 ++++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 11725286e1..8e3615d7bb 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -1230,7 +1230,7 @@ class AssetsController extends Controller // Validate that asset tags were provided in the request if (!$request->filled('asset_tags')) { return response()->json(Helper::formatStandardApiResponse('error', null, - 'No assets selected.'), 400); + trans('admin/hardware/message.no_assets_selected')), 400); } // Convert asset tags from request into collection and fetch matching assets @@ -1240,7 +1240,7 @@ class AssetsController extends Controller // Return error if no assets were found for the provided tags if ($assets->isEmpty()) { return response()->json(Helper::formatStandardApiResponse('error', null, - 'Asset does not exist.'), 404); + trans('admin/hardware/message.does_not_exist')), 404); } try { @@ -1287,14 +1287,14 @@ class AssetsController extends Controller return response()->json(Helper::formatStandardApiResponse('success', [ 'pdf' => $encoded_content - ], 'Labels were successfully generated.')); + ], trans('admin/hardware/message.labels_generated'))); } catch (\Exception $e) { return response()->json(Helper::formatStandardApiResponse('error', [ 'error_message' => $e->getMessage(), 'error_line' => $e->getLine(), 'error_file' => $e->getFile() - ], 'Error while generating labels.'), 500); + ], trans('admin/hardware/message.error_generating_labels')), 500); } } catch (\Exception $e) { return response()->json(Helper::formatStandardApiResponse('error', [ diff --git a/resources/lang/en-US/admin/hardware/message.php b/resources/lang/en-US/admin/hardware/message.php index df68f28395..222cbc439e 100644 --- a/resources/lang/en-US/admin/hardware/message.php +++ b/resources/lang/en-US/admin/hardware/message.php @@ -2,13 +2,16 @@ return [ - 'undeployable' => 'Warning: This asset has been marked as currently undeployable. If this status has changed, please update the asset status.', - 'does_not_exist' => 'Asset does not exist.', - 'does_not_exist_var'=> 'Asset with tag :asset_tag not found.', - 'no_tag' => 'No asset tag provided.', + 'undeployable' => 'Warning: This asset has been marked as currently undeployable. If this status has changed, please update the asset status.', + 'does_not_exist' => 'Asset does not exist.', + 'does_not_exist_var' => 'Asset with tag :asset_tag not found.', + 'no_tag' => 'No asset tag provided.', 'does_not_exist_or_not_requestable' => 'That asset does not exist or is not requestable.', - 'assoc_users' => 'This asset is currently checked out to a user and cannot be deleted. Please check the asset in first, and then try deleting again. ', + 'assoc_users' => 'This asset is currently checked out to a user and cannot be deleted. Please check the asset in first, and then try deleting again. ', 'warning_audit_date_mismatch' => 'This asset\'s next audit date (:next_audit_date) is before the last audit date (:last_audit_date). Please update the next audit date.', + 'labels_generated' => 'Labels were successfully generated.', + 'error_generating_labels' => 'Error while generating labels.', + 'no_assets_selected' => 'No assets selected.', 'create' => [ 'error' => 'Asset was not created, please try again. :(', From 5639a84d90a3c2467bd2566dc54b7ae2e2534d08 Mon Sep 17 00:00:00 2001 From: NebelKreis Date: Tue, 19 Nov 2024 14:16:58 +0100 Subject: [PATCH 05/18] Feature: Added API route for asset label generation --- routes/api.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/routes/api.php b/routes/api.php index 0581a04682..e183cd1228 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1282,4 +1282,14 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi ], 404); }); // end fallback routes + /** + * Generate label routes + */ + Route::post('hardware/labels', [ + Api\AssetsController::class, + 'getLabels' + ])->name('api.assets.labels'); + // end generate label routes + + }); // end API routes From b513575643341152938cb93800846c3be497016d Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 19 Nov 2024 09:10:52 -0800 Subject: [PATCH 06/18] add testing example var MAIL_FROM_ADDR --- .env.testing.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.testing.example b/.env.testing.example index 26211f95c3..6090cd910f 100644 --- a/.env.testing.example +++ b/.env.testing.example @@ -17,3 +17,5 @@ DB_PORT=3306 DB_DATABASE=null DB_USERNAME=null DB_PASSWORD=null + +MAIL_FROM_ADDR=you@example.com From 99de639be42f910c83b4b279bfbe1af8c42f6ba1 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Tue, 19 Nov 2024 09:32:11 -0800 Subject: [PATCH 07/18] fix mail.from.address variables in config, simplify envelop[ variable --- app/Mail/CheckinAccessoryMail.php | 2 +- app/Mail/CheckinAssetMail.php | 2 +- app/Mail/CheckinLicenseMail.php | 2 +- app/Mail/CheckoutAccessoryMail.php | 2 +- app/Mail/CheckoutAssetMail.php | 2 +- app/Mail/CheckoutConsumableMail.php | 2 +- app/Mail/CheckoutLicenseMail.php | 2 +- config/mail.php | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Mail/CheckinAccessoryMail.php b/app/Mail/CheckinAccessoryMail.php index d573a4436f..cddd1bfab9 100644 --- a/app/Mail/CheckinAccessoryMail.php +++ b/app/Mail/CheckinAccessoryMail.php @@ -34,7 +34,7 @@ class CheckinAccessoryMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); + $from = new Address(config('mail.from.address')); return new Envelope( from: $from, diff --git a/app/Mail/CheckinAssetMail.php b/app/Mail/CheckinAssetMail.php index cf660b21ea..d348100eba 100644 --- a/app/Mail/CheckinAssetMail.php +++ b/app/Mail/CheckinAssetMail.php @@ -43,7 +43,7 @@ class CheckinAssetMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); + $from = new Address(config('mail.from.address')); return new Envelope( from: $from, diff --git a/app/Mail/CheckinLicenseMail.php b/app/Mail/CheckinLicenseMail.php index 709879ec1b..8ebc946c00 100644 --- a/app/Mail/CheckinLicenseMail.php +++ b/app/Mail/CheckinLicenseMail.php @@ -34,7 +34,7 @@ class CheckinLicenseMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); + $from = new Address(config('mail.from.address')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutAccessoryMail.php b/app/Mail/CheckoutAccessoryMail.php index 1c3f53402d..9d7d3dba05 100644 --- a/app/Mail/CheckoutAccessoryMail.php +++ b/app/Mail/CheckoutAccessoryMail.php @@ -37,7 +37,7 @@ class CheckoutAccessoryMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); + $from = new Address(config('mail.from.address')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutAssetMail.php b/app/Mail/CheckoutAssetMail.php index 3a1480e8bd..d5c5e23fae 100644 --- a/app/Mail/CheckoutAssetMail.php +++ b/app/Mail/CheckoutAssetMail.php @@ -52,7 +52,7 @@ class CheckoutAssetMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); + $from = new Address(config('mail.from.address')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutConsumableMail.php b/app/Mail/CheckoutConsumableMail.php index 833b562699..ec68125ef3 100644 --- a/app/Mail/CheckoutConsumableMail.php +++ b/app/Mail/CheckoutConsumableMail.php @@ -38,7 +38,7 @@ class CheckoutConsumableMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); + $from = new Address(config('mail.from.address')); return new Envelope( from: $from, diff --git a/app/Mail/CheckoutLicenseMail.php b/app/Mail/CheckoutLicenseMail.php index 691169d8ac..80b226d9bf 100644 --- a/app/Mail/CheckoutLicenseMail.php +++ b/app/Mail/CheckoutLicenseMail.php @@ -36,7 +36,7 @@ class CheckoutLicenseMail extends Mailable */ public function envelope(): Envelope { - $from = new Address(config('mail.from.address'),config('MAIL_ENV_FROM_ADDR')); + $from = new Address(config('mail.from.address')); return new Envelope( from: $from, diff --git a/config/mail.php b/config/mail.php index 4f39fac842..16025bde20 100755 --- a/config/mail.php +++ b/config/mail.php @@ -237,7 +237,7 @@ return [ */ 'from' => [ - 'address' => env('MAIL_FROM_ADDR', null), + 'address' => env('MAIL_FROM_ADDR', env('MAIL_ENV_FROM_ADDR')), 'name' => env('MAIL_FROM_NAME', null), ], From 51a0767be94ab22c0dd35636ed00399a275e8d98 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 19 Nov 2024 16:44:19 -0800 Subject: [PATCH 08/18] Add tests around deleting component via ui --- .../Components/Ui/DeleteComponentTest.php | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/Feature/Components/Ui/DeleteComponentTest.php diff --git a/tests/Feature/Components/Ui/DeleteComponentTest.php b/tests/Feature/Components/Ui/DeleteComponentTest.php new file mode 100644 index 0000000000..a0b8131532 --- /dev/null +++ b/tests/Feature/Components/Ui/DeleteComponentTest.php @@ -0,0 +1,88 @@ +create(); + + $this->actingAs(User::factory()->create()) + ->delete(route('components.destroy', $component->id)) + ->assertForbidden(); + } + + public function testHandlesNonExistentComponent() + { + $this->actingAs(User::factory()->deleteComponents()->create()) + ->delete(route('components.destroy', 10000)) + ->assertSessionHas('error'); + } + + public function testCanDeleteComponent() + { + $component = Component::factory()->create(); + + $this->actingAs(User::factory()->deleteComponents()->create()) + ->delete(route('components.destroy', $component->id)) + ->assertSessionHas('success') + ->assertRedirect(route('components.index')); + + $this->assertSoftDeleted($component); + } + + public function testDeletingComponentRemovesComponentImage() + { + Storage::fake('public'); + + $component = Component::factory()->create(['image' => 'component-image.jpg']); + + Storage::disk('public')->put('components/component-image.jpg', 'content'); + + Storage::disk('public')->assertExists('components/component-image.jpg'); + + $this->actingAs(User::factory()->deleteComponents()->create())->delete(route('components.destroy', $component->id)); + + Storage::disk('public')->assertMissing('components/component-image.jpg'); + } + + public function testDeletingComponentIsLogged() + { + $user = User::factory()->deleteComponents()->create(); + $component = Component::factory()->create(); + + $this->actingAs($user)->delete(route('components.destroy', $component->id)); + + $this->assertDatabaseHas('action_logs', [ + 'created_by' => $user->id, + 'action_type' => 'delete', + 'item_type' => Component::class, + 'item_id' => $component->id, + ]); + } + + public function testAdheresToFullMultipleCompaniesSupportScoping() + { + $this->settings->enableMultipleFullCompanySupport(); + + [$companyA, $companyB] = Company::factory()->count(2)->create(); + + $userInCompanyA = User::factory()->for($companyA)->create(); + $componentForCompanyB = Component::factory()->for($companyB)->create(); + + $this->actingAs($userInCompanyA) + ->delete(route('components.destroy', $componentForCompanyB->id)) + ->assertSessionHas('error'); + + $this->assertNotSoftDeleted($componentForCompanyB); + } +} From 54f5f46e31239eb63f42a56f6527d82c7cfb9ef1 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Tue, 19 Nov 2024 16:44:42 -0800 Subject: [PATCH 09/18] Improve check for image existence --- app/Http/Controllers/Components/ComponentsController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Components/ComponentsController.php b/app/Http/Controllers/Components/ComponentsController.php index c4d9bcec36..62dc25cf1d 100644 --- a/app/Http/Controllers/Components/ComponentsController.php +++ b/app/Http/Controllers/Components/ComponentsController.php @@ -193,7 +193,7 @@ class ComponentsController extends Controller $this->authorize('delete', $component); // Remove the image if one exists - if (Storage::disk('public')->exists('components/'.$component->image)) { + if ($component->image && Storage::disk('public')->exists('components/' . $component->image)) { try { Storage::disk('public')->delete('components/'.$component->image); } catch (\Exception $e) { From 61eff02fcb64710fda8630e0481088138e807eed Mon Sep 17 00:00:00 2001 From: NebelKreis Date: Wed, 20 Nov 2024 10:27:22 +0100 Subject: [PATCH 10/18] Feature: Added category to asset checkout email --- resources/views/mail/markdown/checkout-asset.blade.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/views/mail/markdown/checkout-asset.blade.php b/resources/views/mail/markdown/checkout-asset.blade.php index aae0b21584..72e5860e2c 100644 --- a/resources/views/mail/markdown/checkout-asset.blade.php +++ b/resources/views/mail/markdown/checkout-asset.blade.php @@ -16,6 +16,9 @@ @if (($item->name!=$item->asset_tag)) | **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} | @endif +@if (isset($item->model->category)) +| **{{ trans('general.category') }}** | {{ $item->model->category->name }} | +@endif @if (isset($item->manufacturer)) | **{{ trans('general.manufacturer') }}** | {{ $item->manufacturer->name }} | @endif From 4304af0508784ecc9b0e2d67d0d3617aac028a42 Mon Sep 17 00:00:00 2001 From: NebelKreis Date: Wed, 20 Nov 2024 10:27:37 +0100 Subject: [PATCH 11/18] Feature: Added category to asset checkin email --- resources/views/mail/markdown/checkin-asset.blade.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/views/mail/markdown/checkin-asset.blade.php b/resources/views/mail/markdown/checkin-asset.blade.php index 523ba66aa1..82e0137914 100644 --- a/resources/views/mail/markdown/checkin-asset.blade.php +++ b/resources/views/mail/markdown/checkin-asset.blade.php @@ -16,6 +16,9 @@ @if (($item->name!=$item->asset_tag)) | **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} | @endif +@if (isset($item->model->category)) +| **{{ trans('general.category') }}** | {{ $item->model->category->name }} | +@endif @if (isset($item->manufacturer)) | **{{ trans('general.manufacturer') }}** | {{ $item->manufacturer->name }} | @endif From eb026248a8534bf30c12ab72df37fef11eeeebc5 Mon Sep 17 00:00:00 2001 From: NebelKreis Date: Wed, 20 Nov 2024 10:59:59 +0100 Subject: [PATCH 12/18] Feature: Added category to asset acceptance/decline email --- .../views/notifications/markdown/asset-acceptance.blade.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/views/notifications/markdown/asset-acceptance.blade.php b/resources/views/notifications/markdown/asset-acceptance.blade.php index c5c2a2fa27..65446f8a30 100644 --- a/resources/views/notifications/markdown/asset-acceptance.blade.php +++ b/resources/views/notifications/markdown/asset-acceptance.blade.php @@ -22,6 +22,9 @@ @if ((isset($item_tag)) && ($item_tag!='')) | **{{ trans('mail.asset_tag') }}** | {{ $item_tag }} | @endif +@if (isset($item->model->category)) +| **{{ trans('general.category') }}** | {{ $item->model->category->name }} | +@endif @if ((isset($item_model)) && ($item_model!='')) | **{{ trans('mail.asset_name') }}** | {{ $item_model }} | @endif From 7618169d7906f2cff00951a366155d2ef70359ac Mon Sep 17 00:00:00 2001 From: NebelKreis Date: Wed, 20 Nov 2024 11:22:19 +0100 Subject: [PATCH 13/18] Feature: Added category to asset requested email --- .../views/notifications/markdown/asset-requested.blade.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/views/notifications/markdown/asset-requested.blade.php b/resources/views/notifications/markdown/asset-requested.blade.php index 56ceefb4a7..a634993f99 100644 --- a/resources/views/notifications/markdown/asset-requested.blade.php +++ b/resources/views/notifications/markdown/asset-requested.blade.php @@ -18,6 +18,9 @@ @if ((isset($item->asset_tag)) && ($item->asset_tag!='')) | **{{ trans('mail.asset_tag') }}** | {{ $item->asset_tag }} | @endif +@if (isset($item->model->category)) +| **{{ trans('general.category') }}** | {{ $item->model->category->name }} | +@endif @if ((isset($item->name)) && ($item->name!='')) | **{{ trans('mail.asset_name') }}** | {{ $item->name }} | @endif From d5c141dc59d305996da66a53dc05eb4ceeb99466 Mon Sep 17 00:00:00 2001 From: NebelKreis Date: Wed, 20 Nov 2024 11:55:48 +0100 Subject: [PATCH 14/18] Fix: Removed setting validation as it is not strictly necessary --- app/Http/Controllers/Api/AssetsController.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 8e3615d7bb..7a9f582213 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -1245,17 +1245,11 @@ class AssetsController extends Controller try { $settings = Setting::getSettings(); - - if (!$settings) { - throw new \Exception('Settings could not be loaded'); - } // Check if logo file exists in storage and disable logo if not found // This prevents errors when trying to include a non-existent logo in the PDF - $original_logo = $settings->label_logo; - if ($original_logo && !Storage::disk('public')->exists('/' . $original_logo)) { - $settings->label_logo = null; - } + $settings->label_logo = ($original_logo = $settings->label_logo) && !Storage::disk('public')->exists('/' . $original_logo) ? null : $settings->label_logo; + $label = new Label(); From f7c1fc852d46e508302c0a3f288f1a40d2d78830 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 20 Nov 2024 08:29:28 -0800 Subject: [PATCH 15/18] no checked out to email doesnt trigger a 500 --- app/Listeners/CheckoutableListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 6037ee1ec4..145abb1687 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -307,7 +307,7 @@ class CheckoutableListener return $event->checkedOutTo->manager?->email ?? ''; } else{ - return $event->checkedOutTo->email; + return $event->checkedOutTo?->email ?? ''; } } From 9bbd802fce734161d957993cb89755fd284abf20 Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 20 Nov 2024 12:33:58 -0800 Subject: [PATCH 16/18] Use database file for sqlite in GitHub Action for tests --- .github/workflows/tests-sqlite.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-sqlite.yml b/.github/workflows/tests-sqlite.yml index 8bf0115169..220194314f 100644 --- a/.github/workflows/tests-sqlite.yml +++ b/.github/workflows/tests-sqlite.yml @@ -43,6 +43,9 @@ jobs: cp -v .env.testing.example .env cp -v .env.testing.example .env.testing + - name: Create database file + run: touch database/database.sqlite + - name: Install Dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist @@ -57,5 +60,5 @@ jobs: - name: Execute tests (Unit and Feature tests) via PHPUnit env: - DB_CONNECTION: sqlite_testing + DB_CONNECTION: sqlite run: php artisan test From 6969b8be47ea8163c9abe4fc35398c5f3d5dd0a2 Mon Sep 17 00:00:00 2001 From: Godfrey M Date: Wed, 20 Nov 2024 14:25:57 -0800 Subject: [PATCH 17/18] remove the changes to mail.from.address default --- config/mail.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/mail.php b/config/mail.php index 16025bde20..4f39fac842 100755 --- a/config/mail.php +++ b/config/mail.php @@ -237,7 +237,7 @@ return [ */ 'from' => [ - 'address' => env('MAIL_FROM_ADDR', env('MAIL_ENV_FROM_ADDR')), + 'address' => env('MAIL_FROM_ADDR', null), 'name' => env('MAIL_FROM_NAME', null), ], From d65254c4ec3c5b58160837f436747d63021d4a6a Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 20 Nov 2024 15:34:08 -0800 Subject: [PATCH 18/18] Add tests for component checkout api endpoint --- .../Checkouts/Api/ComponentCheckoutTest.php | 140 ++++++++++++++++++ tests/Support/CustomTestMacros.php | 21 +++ 2 files changed, 161 insertions(+) create mode 100644 tests/Feature/Checkouts/Api/ComponentCheckoutTest.php diff --git a/tests/Feature/Checkouts/Api/ComponentCheckoutTest.php b/tests/Feature/Checkouts/Api/ComponentCheckoutTest.php new file mode 100644 index 0000000000..179e9329e2 --- /dev/null +++ b/tests/Feature/Checkouts/Api/ComponentCheckoutTest.php @@ -0,0 +1,140 @@ +create(); + + $this->actingAsForApi(User::factory()->create()) + ->postJson(route('api.components.checkout', $component->id)) + ->assertForbidden(); + } + + public function testCannotCheckoutNonExistentComponent() + { + $this->actingAsForApi(User::factory()->checkoutComponents()->create()) + ->postJson(route('api.components.checkout', 1000)) + ->assertOk() + ->assertStatusMessageIs('error') + ->assertMessagesAre('Component does not exist.'); + } + + public function testCheckingOutComponentRequiresValidFields() + { + $component = Component::factory()->create(); + + $this->actingAsForApi(User::factory()->checkoutComponents()->create()) + ->postJson(route('api.components.checkout', $component->id), [ + // + ]) + ->assertOk() + ->assertStatusMessageIs('error') + ->assertPayloadContains('assigned_to') + ->assertPayloadContains('assigned_qty'); + } + + public function testCannotCheckoutComponentIfRequestedAmountIsMoreThanComponentQuantity() + { + $asset = Asset::factory()->create(); + $component = Component::factory()->create(['qty' => 2]); + + $this->actingAsForApi(User::factory()->checkoutComponents()->create()) + ->postJson(route('api.components.checkout', $component->id), [ + 'assigned_to' => $asset->id, + 'assigned_qty' => 3, + ]) + ->assertOk() + ->assertStatusMessageIs('error') + ->assertMessagesAre(trans('admin/components/message.checkout.unavailable', ['remaining' => 2, 'requested' => 3])); + } + + public function testCannotCheckoutComponentIfRequestedAmountIsMoreThanWhatIsRemaining() + { + $asset = Asset::factory()->create(); + $component = Component::factory()->create(['qty' => 2]); + $component->assets()->attach($component->id, [ + 'component_id' => $component->id, + 'created_at' => Carbon::now(), + 'assigned_qty' => 1, + 'asset_id' => $asset->id, + ]); + + $this->actingAsForApi(User::factory()->checkoutComponents()->create()) + ->postJson(route('api.components.checkout', $component->id), [ + 'assigned_to' => $asset->id, + 'assigned_qty' => 3, + ]) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testCanCheckoutComponent() + { + $user = User::factory()->checkoutComponents()->create(); + $asset = Asset::factory()->create(); + $component = Component::factory()->create(); + + $this->actingAsForApi($user) + ->postJson(route('api.components.checkout', $component->id), [ + 'assigned_to' => $asset->id, + 'assigned_qty' => 1, + ]) + ->assertOk() + ->assertStatusMessageIs('success'); + + $this->assertTrue($component->assets->first()->is($asset)); + } + + public function testComponentCheckoutIsLogged() + { + $user = User::factory()->checkoutComponents()->create(); + $location = Location::factory()->create(); + $asset = Asset::factory()->create(['location_id' => $location->id]); + $component = Component::factory()->create(); + + $this->actingAsForApi($user) + ->postJson(route('api.components.checkout', $component->id), [ + 'assigned_to' => $asset->id, + 'assigned_qty' => 1, + ]); + + $this->assertDatabaseHas('action_logs', [ + 'created_by' => $user->id, + 'action_type' => 'checkout', + 'target_id' => $asset->id, + 'target_type' => Asset::class, + 'location_id' => $location->id, + 'item_type' => Component::class, + 'item_id' => $component->id, + ]); + } + + public function testAdheresToFullMultipleCompaniesSupportScoping() + { + [$companyA, $companyB] = Company::factory()->count(2)->create(); + + $userForCompanyA = User::factory()->for($companyA)->create(); + $assetForCompanyB = Asset::factory()->for($companyB)->create(); + $componentForCompanyB = Component::factory()->for($companyB)->create(); + + $this->actingAsForApi($userForCompanyA) + ->postJson(route('api.components.checkout', $componentForCompanyB->id), [ + 'assigned_to' => $assetForCompanyB->id, + 'assigned_qty' => 1, + ]) + ->assertForbidden(); + } +} diff --git a/tests/Support/CustomTestMacros.php b/tests/Support/CustomTestMacros.php index 9c0caaf959..956235f461 100644 --- a/tests/Support/CustomTestMacros.php +++ b/tests/Support/CustomTestMacros.php @@ -121,5 +121,26 @@ trait CustomTestMacros return $this; } ); + + TestResponse::macro( + 'assertPayloadContains', + function (array|string $keys) { + Assert::assertArrayHasKey('payload', $this, 'Response did not contain a payload'); + + if (is_string($keys)) { + $keys = [$keys]; + } + + foreach ($keys as $key) { + Assert::assertArrayHasKey( + $key, + $this['payload'], + "Response messages did not contain the key: {$key}" + ); + } + + return $this; + } + ); } }