diff --git a/.env.docker b/.env.docker index 87897b10db..7b9e2000cf 100644 --- a/.env.docker +++ b/.env.docker @@ -45,6 +45,7 @@ DB_SSL_KEY_PATH=null DB_SSL_CERT_PATH=null DB_SSL_CA_PATH=null DB_SSL_CIPHER=null +DB_SSL_VERIFY_SERVER=null # -------------------------------------------- # REQUIRED: OUTGOING MAIL SERVER SETTINGS diff --git a/.env.dusk.example b/.env.dusk.example index 074f6fc3d7..3a9765dc19 100644 --- a/.env.dusk.example +++ b/.env.dusk.example @@ -36,6 +36,7 @@ DB_SSL_KEY_PATH=null DB_SSL_CERT_PATH=null DB_SSL_CA_PATH=null DB_SSL_CIPHER=null +DB_SSL_VERIFY_SERVER=null # -------------------------------------------- # REQUIRED: OUTGOING MAIL SERVER SETTINGS diff --git a/.env.example b/.env.example index fd90391973..8f3e5a2d69 100644 --- a/.env.example +++ b/.env.example @@ -42,6 +42,7 @@ DB_SSL_KEY_PATH=null DB_SSL_CERT_PATH=null DB_SSL_CA_PATH=null DB_SSL_CIPHER=null +DB_SSL_VERIFY_SERVER=null # -------------------------------------------- # REQUIRED: OUTGOING MAIL SERVER SETTINGS diff --git a/app/Console/Commands/LdapSync.php b/app/Console/Commands/LdapSync.php index 05f60ee4e1..bd91c2bf25 100755 --- a/app/Console/Commands/LdapSync.php +++ b/app/Console/Commands/LdapSync.php @@ -390,7 +390,7 @@ class LdapSync extends Command $user->location_id = $location->id; } } - + $location = null; $user->ldap_import = 1; $errors = ''; diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 92f1038cbd..57307acbb3 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -665,25 +665,26 @@ class AssetsController extends Controller $model = AssetModel::find($asset->model_id); // 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->field_encrypted == '1') { - if (Gate::allows('admin')) { - $asset->{$field->db_column} = Crypt::encrypt($field_val); - } - } if ($field->element == 'checkbox') { if(is_array($field_val)) { $field_val = implode(',', $field_val); - $asset->{$field->db_column} = $field_val; } } - else { - $asset->{$field->db_column} = $field_val; + if ($field->field_encrypted == '1') { + if (Gate::allows('admin')) { + $field_val = Crypt::encrypt($field_val); + } else { + $problems_updating_encrypted_custom_fields = true; + continue; + } } + $asset->{$field->db_column} = $field_val; } } } @@ -709,8 +710,11 @@ class AssetsController extends Controller $asset->image = $asset->getImageUrl(); } - return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success'))); - return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.update.success'))); + if ($problems_updating_encrypted_custom_fields) { + return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning'))); + } else { + return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success'))); + } } return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200); diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 56b61dccf7..206941f252 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -276,6 +276,7 @@ class UsersController extends Controller $users = $users->withTrashed(); } + // Apply companyable scope $users = Company::scopeCompanyables($users); @@ -406,7 +407,10 @@ class UsersController extends Controller public function show($id) { $this->authorize('view', User::class); + $user = User::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count')->findOrFail($id); + $user = Company::scopeCompanyables($user)->find($id); + $this->authorize('update', $user); return (new UsersTransformer)->transformUser($user); } @@ -426,6 +430,8 @@ class UsersController extends Controller $this->authorize('update', User::class); $user = User::findOrFail($id); + $user = Company::scopeCompanyables($user)->find($id); + $this->authorize('update', $user); /** * This is a janky hack to prevent people from changing admin demo user data on the public demo. @@ -462,6 +468,7 @@ class UsersController extends Controller if (! Auth::user()->isSuperUser()) { unset($permissions_array['superuser']); } + $user->permissions = $permissions_array; } @@ -484,6 +491,7 @@ class UsersController extends Controller // Check if the request has groups passed and has a value if ($request->filled('groups')) { + $validator = Validator::make($request->all(), [ 'groups.*' => 'integer|exists:permission_groups,id', ]); @@ -491,10 +499,19 @@ class UsersController extends Controller if ($validator->fails()){ return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors())); } - $user->groups()->sync($request->input('groups')); + + // Only save groups if the user is a superuser + if (Auth::user()->isSuperUser()) { + $user->groups()->sync($request->input('groups')); + } + // The groups field has been passed but it is null, so we should blank it out } elseif ($request->has('groups')) { - $user->groups()->sync([]); + + // Only save groups if the user is a superuser + if (Auth::user()->isSuperUser()) { + $user->groups()->sync($request->input('groups')); + } } @@ -515,37 +532,41 @@ class UsersController extends Controller public function destroy($id) { $this->authorize('delete', User::class); - $user = User::findOrFail($id); + $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed(); + $user = Company::scopeCompanyables($user)->find($id); $this->authorize('delete', $user); - if (($user->assets) && ($user->assets->count() > 0)) { - return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete_has_assets'))); - } + if ($user) { - if (($user->licenses) && ($user->licenses->count() > 0)) { - return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has '.$user->licenses->count().' license(s) associated with them and cannot be deleted.')); - } - - if (($user->accessories) && ($user->accessories->count() > 0)) { - return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has '.$user->accessories->count().' accessories associated with them.')); - } - - if (($user->managedLocations()) && ($user->managedLocations()->count() > 0)) { - return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has '.$user->managedLocations()->count().' locations that they manage.')); - } - - if ($user->delete()) { - - // Remove the user's avatar if they have one - if (Storage::disk('public')->exists('avatars/'.$user->avatar)) { - try { - Storage::disk('public')->delete('avatars/'.$user->avatar); - } catch (\Exception $e) { - Log::debug($e); - } + if (($user->assets) && ($user->assets->count() > 0)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete_has_assets'))); } - return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.delete'))); + if (($user->licenses) && ($user->licenses->count() > 0)) { + return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has ' . $user->licenses->count() . ' license(s) associated with them and cannot be deleted.')); + } + + if (($user->accessories) && ($user->accessories->count() > 0)) { + return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has ' . $user->accessories->count() . ' accessories associated with them.')); + } + + if (($user->managedLocations()) && ($user->managedLocations()->count() > 0)) { + return response()->json(Helper::formatStandardApiResponse('error', null, 'This user still has ' . $user->managedLocations()->count() . ' locations that they manage.')); + } + + if ($user->delete()) { + + // Remove the user's avatar if they have one + if (Storage::disk('public')->exists('avatars/' . $user->avatar)) { + try { + Storage::disk('public')->delete('avatars/' . $user->avatar); + } catch (\Exception $e) { + \Log::debug($e); + } + } + + return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.delete'))); + } } return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete'))); @@ -563,6 +584,11 @@ class UsersController extends Controller { $this->authorize('view', User::class); $this->authorize('view', Asset::class); + + $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed(); + $user = Company::scopeCompanyables($user)->find($id); + $this->authorize('view', $user); + $assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model'); @@ -598,7 +624,10 @@ class UsersController extends Controller */ public function emailAssetList(Request $request, $id) { + $this->authorize('update', User::class); $user = User::findOrFail($id); + $user = Company::scopeCompanyables($user)->find($id); + $this->authorize('update', $user); if (empty($user->email)) { return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.inventorynotification.error'))); @@ -622,6 +651,7 @@ class UsersController extends Controller $this->authorize('view', User::class); $this->authorize('view', Consumable::class); $user = User::findOrFail($id); + $this->authorize('update', $user); $consumables = $user->consumables; return (new ConsumablesTransformer)->transformConsumables($consumables, $consumables->count(), $request); } @@ -638,6 +668,7 @@ class UsersController extends Controller { $this->authorize('view', User::class); $user = User::findOrFail($id); + $this->authorize('view', $user); $this->authorize('view', Accessory::class); $accessories = $user->accessories; @@ -658,6 +689,7 @@ class UsersController extends Controller $this->authorize('view', License::class); if ($user = User::where('id', $id)->withTrashed()->first()) { + $this->authorize('update', $user); $licenses = $user->licenses()->get(); return (new LicensesTransformer())->transformLicenses($licenses, $licenses->count()); } @@ -681,6 +713,7 @@ class UsersController extends Controller if ($request->filled('id')) { try { $user = User::find($request->get('id')); + $this->authorize('update', $user); $user->two_factor_secret = null; $user->two_factor_enrolled = 0; $user->saveQuietly(); diff --git a/app/Http/Controllers/AssetModelsFilesController.php b/app/Http/Controllers/AssetModelsFilesController.php index a5419b428d..a4472c3504 100644 --- a/app/Http/Controllers/AssetModelsFilesController.php +++ b/app/Http/Controllers/AssetModelsFilesController.php @@ -38,7 +38,7 @@ class AssetModelsFilesController extends Controller $file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$model->id,$file); - $model->logUpload($file_name, e($request->get('notes'))); + $model->logUpload($file_name, $request->get('notes')); } return redirect()->back()->with('success', trans('general.file_upload_success')); diff --git a/app/Http/Controllers/Assets/AssetCheckinController.php b/app/Http/Controllers/Assets/AssetCheckinController.php index 82cb98abe9..30ffd8bba2 100644 --- a/app/Http/Controllers/Assets/AssetCheckinController.php +++ b/app/Http/Controllers/Assets/AssetCheckinController.php @@ -39,6 +39,12 @@ class AssetCheckinController extends Controller $this->authorize('checkin', $asset); + // This asset is already checked in, redirect + + if (is_null($asset->assignedTo)) { + return redirect()->route('hardware.index')->with('error', trans('admin/hardware/message.checkin.already_checked_in')); + } + return view('hardware/checkin', compact('asset'))->with('statusLabel_list', Helper::statusLabelList())->with('backto', $backto); } diff --git a/app/Http/Controllers/Assets/AssetFilesController.php b/app/Http/Controllers/Assets/AssetFilesController.php index 7f4258bda2..7debfb479c 100644 --- a/app/Http/Controllers/Assets/AssetFilesController.php +++ b/app/Http/Controllers/Assets/AssetFilesController.php @@ -38,7 +38,7 @@ class AssetFilesController extends Controller foreach ($request->file('file') as $file) { $file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file); - $asset->logUpload($file_name, e($request->get('notes'))); + $asset->logUpload($file_name, $request->get('notes')); } return redirect()->back()->with('success', trans('admin/hardware/message.upload.success')); diff --git a/app/Http/Controllers/LabelsController.php b/app/Http/Controllers/LabelsController.php index 799d910384..1c5715b35f 100755 --- a/app/Http/Controllers/LabelsController.php +++ b/app/Http/Controllers/LabelsController.php @@ -14,6 +14,7 @@ use App\Models\Setting; use App\Models\Supplier; use App\Models\User; use App\View\Label as LabelView; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; class LabelsController extends Controller @@ -21,9 +22,9 @@ class LabelsController extends Controller /** * Returns the Label view with test data * - * @author Grant Le Roux - * @param string $labelName + * @param string $labelName * @return \Illuminate\Contracts\View\View + * @author Grant Le Roux */ public function show(string $labelName) { @@ -66,7 +67,7 @@ class LabelsController extends Controller $exampleAsset->model->category->id = 999999; $exampleAsset->model->category->name = trans('admin/labels/table.example_category'); - $customFieldColumns = CustomField::all()->pluck('db_column'); + $customFieldColumns = CustomField::where('field_encrypted', '=', 0)->pluck('db_column'); collect(explode(';', Setting::getSettings()->label2_fields)) ->filter() diff --git a/app/Http/Controllers/Licenses/LicensesController.php b/app/Http/Controllers/Licenses/LicensesController.php index c55181c518..268c3f8b66 100755 --- a/app/Http/Controllers/Licenses/LicensesController.php +++ b/app/Http/Controllers/Licenses/LicensesController.php @@ -11,6 +11,7 @@ use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * This controller handles all actions related to Licenses for @@ -289,4 +290,106 @@ class LicensesController extends Controller ->with('item', $license) ->with('maintained_list', $maintained_list); } + + /** + * Exports Licenses to CSV + * + * @author [G. Martinez] + * @since [v6.3] + * @return StreamedResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function getExportLicensesCsv() + { + $this->authorize('view', License::class); + \Debugbar::disable(); + + $response = new StreamedResponse(function () { + // Open output stream + $handle = fopen('php://output', 'w'); + $licenses= License::with('company', + 'manufacturer', + 'category', + 'supplier', + 'adminuser', + 'assignedusers') + ->orderBy('created_at', 'DESC'); + Company::scopeCompanyables($licenses) + ->chunk(500, function ($licenses) use ($handle) { + $headers = [ + // strtolower to prevent Excel from trying to open it as a SYLK file + strtolower(trans('general.id')), + trans('general.company'), + trans('general.name'), + trans('general.serial_number'), + trans('general.purchase_date'), + trans('general.purchase_cost'), + trans('general.order_number'), + trans('general.licenses_available'), + trans('admin/licenses/table.seats'), + trans('general.created_by'), + trans('general.depreciation'), + trans('general.updated_at'), + trans('admin/licenses/table.deleted_at'), + trans('general.email'), + trans('admin/hardware/form.fully_depreciated'), + trans('general.supplier'), + trans('admin/licenses/form.expiration'), + trans('admin/licenses/form.purchase_order'), + trans('admin/licenses/form.termination_date'), + trans('admin/licenses/form.maintained'), + trans('general.manufacturer'), + trans('general.category'), + trans('general.min_amt'), + trans('admin/licenses/form.reassignable'), + trans('general.notes'), + trans('general.created_at'), + ]; + + fputcsv($handle, $headers); + + foreach ($licenses as $license) { + // Add a new row with data + $values = [ + $license->id, + $license->company ? $license->company->name: '', + $license->name, + $license->serial, + $license->purchase_date, + $license->purchase_cost, + $license->order_number, + $license->free_seat_count, + $license->seats, + $license->adminuser->present()->fullName(), + $license->depreciation ? $license->depreciation->name: '', + $license->updated_at, + $license->deleted_at, + $license->email, + ( $license->depreciate == '1') ? trans('general.yes') : trans('general.no'), + ($license->supplier) ? $license->supplier->name: '', + $license->expiration_date, + $license->purchase_order, + $license->termination_date, + ( $license->maintained == '1') ? trans('general.yes') : trans('general.no'), + $license->manufacturer ? $license->manufacturer->name: '', + $license->category ? $license->category->name: '', + $license->min_amt, + ( $license->reassignable == '1') ? trans('general.yes') : trans('general.no'), + $license->notes, + $license->created_at, + ]; + + fputcsv($handle, $values); + } + }); + + // Close the output stream + fclose($handle); + }, 200, [ + 'Content-Type' => 'text/csv; charset=UTF-8', + 'Content-Disposition' => 'attachment; filename="licenses-'.date('Y-m-d-his').'.csv"', + ]); + + return $response; + } } diff --git a/app/Http/Controllers/ReportsController.php b/app/Http/Controllers/ReportsController.php index 6372c37beb..c1ae800342 100644 --- a/app/Http/Controllers/ReportsController.php +++ b/app/Http/Controllers/ReportsController.php @@ -696,7 +696,7 @@ class ReportsController extends Controller ->whereBetween('action_date',[$checkout_start, $checkout_end]) ->pluck('item_id'); - $assets->whereIn('id',$actionlogassets); + $assets->whereIn('assets.id',$actionlogassets); } if (($request->filled('checkin_date_start'))) { diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index 744bd7e619..fc8f10ec8e 100755 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -805,10 +805,9 @@ class SettingsController extends Controller */ public function getLabels() { - return view('settings.labels', [ - 'setting' => Setting::getSettings(), - 'customFields' => CustomField::all(), - ]); + return view('settings.labels') + ->with('setting', Setting::getSettings()) + ->with('customFields', CustomField::where('field_encrypted', '=', 0)->get()); } /** diff --git a/app/Http/Controllers/Users/UsersController.php b/app/Http/Controllers/Users/UsersController.php index 2655e50f75..9316c2dec4 100755 --- a/app/Http/Controllers/Users/UsersController.php +++ b/app/Http/Controllers/Users/UsersController.php @@ -182,8 +182,13 @@ class UsersController extends Controller */ public function edit($id) { - if ($user = User::find($id)) { - $this->authorize('update', $user); + + $this->authorize('update', User::class); + $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed(); + $user = Company::scopeCompanyables($user)->find($id); + + if ($user) { + $permissions = config('permissions'); $groups = Group::pluck('name', 'id'); @@ -210,106 +215,109 @@ class UsersController extends Controller */ public function update(SaveUserRequest $request, $id = null) { - // We need to reverse the UI specific logic for our - // permissions here before we update the user. - $permissions = $request->input('permissions', []); - app('request')->request->set('permissions', $permissions); + $this->authorize('update', User::class); // This is a janky hack to prevent people from changing admin demo user data on the public demo. // The $ids 1 and 2 are special since they are seeded as superadmins in the demo seeder. // Thanks, jerks. You are why we can't have nice things. - snipe if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) { - return redirect()->route('users.index')->with('error', 'Permission denied. You cannot update user information for superadmins on the demo.'); + return redirect()->route('users.index')->with('error', trans('general.permission_denied_superuser_demo')); } - try { - $user = User::findOrFail($id); - } catch (ModelNotFoundException $e) { - return redirect()->route('users.index') - ->with('error', trans('admin/users/message.user_not_found', compact('id'))); - } - $this->authorize('update', $user); - // Figure out of this user was an admin before this edit - $orig_permissions_array = $user->decodePermissions(); - $orig_superuser = '0'; - if (is_array($orig_permissions_array)) { - if (array_key_exists('superuser', $orig_permissions_array)) { - $orig_superuser = $orig_permissions_array['superuser']; + // We need to reverse the UI specific logic for our + // permissions here before we update the user. + $permissions = $request->input('permissions', []); + app('request')->request->set('permissions', $permissions); + + + $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed(); + $user = Company::scopeCompanyables($user)->find($id); + + // User is valid - continue... + if ($user) { + $this->authorize('update', $user); + + // Figure out of this user was an admin before this edit + $orig_permissions_array = $user->decodePermissions(); + $orig_superuser = '0'; + if (is_array($orig_permissions_array)) { + if (array_key_exists('superuser', $orig_permissions_array)) { + $orig_superuser = $orig_permissions_array['superuser']; + } } - } - // Only save groups if the user is a super user - if (Auth::user()->isSuperUser()) { - $user->groups()->sync($request->input('groups')); - } + // Only save groups if the user is a superuser + if (Auth::user()->isSuperUser()) { + $user->groups()->sync($request->input('groups')); + } - // Update the user - if ($request->filled('username')) { + // Update the user fields $user->username = trim($request->input('username')); - } - $user->email = trim($request->input('email')); - $user->first_name = $request->input('first_name'); - $user->last_name = $request->input('last_name'); - $user->two_factor_optin = $request->input('two_factor_optin') ?: 0; - $user->locale = $request->input('locale'); - $user->employee_num = $request->input('employee_num'); - $user->activated = $request->input('activated', 0); - $user->jobtitle = $request->input('jobtitle', null); - $user->phone = $request->input('phone'); - $user->location_id = $request->input('location_id', null); - $user->company_id = Company::getIdForUser($request->input('company_id', null)); - $user->manager_id = $request->input('manager_id', null); - $user->notes = $request->input('notes'); - $user->department_id = $request->input('department_id', null); - $user->address = $request->input('address', null); - $user->city = $request->input('city', null); - $user->state = $request->input('state', null); - $user->country = $request->input('country', null); - // if a user is editing themselves we should always keep activated true - $user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0); - $user->zip = $request->input('zip', null); - $user->remote = $request->input('remote', 0); - $user->vip = $request->input('vip', 0); - $user->website = $request->input('website', null); - $user->start_date = $request->input('start_date', null); - $user->end_date = $request->input('end_date', null); - $user->autoassign_licenses = $request->input('autoassign_licenses', 0); + $user->email = trim($request->input('email')); + $user->first_name = $request->input('first_name'); + $user->last_name = $request->input('last_name'); + $user->two_factor_optin = $request->input('two_factor_optin') ?: 0; + $user->locale = $request->input('locale'); + $user->employee_num = $request->input('employee_num'); + $user->activated = $request->input('activated', 0); + $user->jobtitle = $request->input('jobtitle', null); + $user->phone = $request->input('phone'); + $user->location_id = $request->input('location_id', null); + $user->company_id = Company::getIdForUser($request->input('company_id', null)); + $user->manager_id = $request->input('manager_id', null); + $user->notes = $request->input('notes'); + $user->department_id = $request->input('department_id', null); + $user->address = $request->input('address', null); + $user->city = $request->input('city', null); + $user->state = $request->input('state', null); + $user->country = $request->input('country', null); + // if a user is editing themselves we should always keep activated true + $user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0); + $user->zip = $request->input('zip', null); + $user->remote = $request->input('remote', 0); + $user->vip = $request->input('vip', 0); + $user->website = $request->input('website', null); + $user->start_date = $request->input('start_date', null); + $user->end_date = $request->input('end_date', null); + $user->autoassign_licenses = $request->input('autoassign_licenses', 0); + + // Update the location of any assets checked out to this user + Asset::where('assigned_type', User::class) + ->where('assigned_to', $user->id) + ->update(['location_id' => $request->input('location_id', null)]); + + // Do we want to update the user password? + if ($request->filled('password')) { + $user->password = bcrypt($request->input('password')); + } + + $permissions_array = $request->input('permission'); + + // Strip out the superuser permission if the user isn't a superadmin + if (! Auth::user()->isSuperUser()) { + unset($permissions_array['superuser']); + $permissions_array['superuser'] = $orig_superuser; + } + + $user->permissions = json_encode($permissions_array); + + // Handle uploaded avatar + app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar'); + + if ($user->save()) { + // Redirect to the user page + return redirect()->route('users.index') + ->with('success', trans('admin/users/message.success.update')); + } + + return redirect()->back()->withInput()->withErrors($user->getErrors()); - // Update the location of any assets checked out to this user - Asset::where('assigned_type', User::class) - ->where('assigned_to', $user->id) - ->update(['location_id' => $request->input('location_id', null)]); - // Do we want to update the user password? - if ($request->filled('password')) { - $user->password = bcrypt($request->input('password')); } - $permissions_array = $request->input('permission'); - - // Strip out the superuser permission if the user isn't a superadmin - if (! Auth::user()->isSuperUser()) { - unset($permissions_array['superuser']); - $permissions_array['superuser'] = $orig_superuser; - } - - $user->permissions = json_encode($permissions_array); - - // Handle uploaded avatar - app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar'); - - //\Log::debug(print_r($user, true)); - - // Was the user updated? - if ($user->save()) { - // Redirect to the user page - return redirect()->route('users.index') - ->with('success', trans('admin/users/message.success.update')); - } - - return redirect()->back()->withInput()->withErrors($user->getErrors()); + return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id'))); } /** @@ -323,12 +331,13 @@ class UsersController extends Controller */ public function destroy($id = null) { - try { - // Get user information - $user = User::findOrFail($id); - // Authorize takes care of many of our logic checks now. - $this->authorize('delete', User::class); + $this->authorize('delete', User::class); + $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed(); + $user = Company::scopeCompanyables($user)->find($id); + + + if ($user) { // Check if we are not trying to delete ourselves if ($user->id === Auth::id()) { // Redirect to the user management page @@ -362,16 +371,12 @@ class UsersController extends Controller // Delete the user $user->delete(); - - // Prepare the success message - // Redirect to the user management page return redirect()->route('users.index')->with('success', trans('admin/users/message.success.delete')); - } catch (ModelNotFoundException $e) { - // Prepare the error message - // Redirect to the user management page - return redirect()->route('users.index') - ->with('error', trans('admin/users/message.user_not_found', compact('id'))); } + + return redirect()->route('users.index') + ->with('error', trans('admin/users/message.user_not_found', compact('id'))); + } /** @@ -427,59 +432,25 @@ class UsersController extends Controller */ public function show($userId = null) { - if (! $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($userId)) { - // Redirect to the user management page - return redirect()->route('users.index') - ->with('error', trans('admin/users/message.user_not_found', ['id' => $userId])); - } + // Make sure the user can view users at all + $this->authorize('view', User::class); - $userlog = $user->userlog->load('item'); + $user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed(); + $user = Company::scopeCompanyables($user)->find($userId); + // Make sure they can view this particular user $this->authorize('view', $user); - return view('users/view', compact('user', 'userlog')) - ->with('settings', Setting::getSettings()); - } - - /** - * Unsuspend a user. - * - * @author [A. Gianotto] [] - * @since [v1.0] - * @param int $id - * @return Redirect - * @throws \Illuminate\Auth\Access\AuthorizationException - */ - public function getUnsuspend($id = null) - { - try { - // Get user information - $user = User::findOrFail($id); - $this->authorize('update', $user); - - // Check if we are not trying to unsuspend ourselves - if ($user->id === Auth::id()) { - // Prepare the error message - $error = trans('admin/users/message.error.unsuspend'); - // Redirect to the user management page - return redirect()->route('users.index')->with('error', $error); - } - - // Do we have permission to unsuspend this user? - if ($user->isSuperUser() && ! Auth::user()->isSuperUser()) { - // Redirect to the user management page - return redirect()->route('users.index')->with('error', 'Insufficient permissions!'); - } - - // Redirect to the user management page - return redirect()->route('users.index')->with('success', trans('admin/users/message.success.unsuspend')); - } catch (ModelNotFoundException $e) { - // Redirect to the user management page - return redirect()->route('users.index') - ->with('error', trans('admin/users/message.user_not_found', compact('id'))); + if ($user) { + $userlog = $user->userlog->load('item'); + return view('users/view', compact('user', 'userlog'))->with('settings', Setting::getSettings()); } + + return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', ['id' => $userId])); + } + /** * Return a view containing a pre-populated new user form, * populated with some fields from an existing user. @@ -493,22 +464,34 @@ class UsersController extends Controller public function getClone(Request $request, $id = null) { $this->authorize('create', User::class); + // We need to reverse the UI specific logic for our // permissions here before we update the user. $permissions = $request->input('permissions', []); app('request')->request->set('permissions', $permissions); - try { - // Get the user information - $user_to_clone = User::withTrashed()->find($id); + + $user_to_clone = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed(); + $user_to_clone = Company::scopeCompanyables($user_to_clone)->find($id); + + // Make sure they can view this particular user + $this->authorize('view', $user_to_clone); + + + if ($user_to_clone) { + + $user = clone $user_to_clone; + + // Blank out some fields $user->first_name = ''; $user->last_name = ''; $user->email = substr($user->email, ($pos = strpos($user->email, '@')) !== false ? $pos : 0); $user->id = null; - // Get this user groups + // Get this user's groups $userGroups = $user_to_clone->groups()->pluck('name', 'id'); + // Get all the available permissions $permissions = config('permissions'); $clonedPermissions = $user_to_clone->decodePermissions(); @@ -517,16 +500,14 @@ class UsersController extends Controller // Show the page return view('users/edit', compact('permissions', 'userPermissions')) - ->with('user', $user) - ->with('groups', Group::pluck('name', 'id')) - ->with('userGroups', $userGroups) - ->with('clone_user', $user_to_clone); - } catch (ModelNotFoundException $e) { - // Prepare the error message - // Redirect to the user management page - return redirect()->route('users.index') - ->with('error', trans('admin/users/message.user_not_found', compact('id'))); + ->with('user', $user) + ->with('groups', Group::pluck('name', 'id')) + ->with('userGroups', $userGroups) + ->with('clone_user', $user_to_clone); } + + return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id'))); + } /** @@ -546,8 +527,20 @@ class UsersController extends Controller // Open output stream $handle = fopen('php://output', 'w'); - User::with('assets', 'accessories', 'consumables', 'department', 'licenses', 'manager', 'groups', 'userloc', 'company') - ->orderBy('created_at', 'DESC') + $users = User::with( + 'assets', + 'accessories', + 'consumables', + 'department', + 'licenses', + 'manager', + 'groups', + 'userloc', + 'company' + )->orderBy('created_at', 'DESC'); + + // FMCS scoping + Company::scopeCompanyables($users) ->chunk(500, function ($users) use ($handle) { $headers = [ // strtolower to prevent Excel from trying to open it as a SYLK file @@ -565,7 +558,7 @@ class UsersController extends Controller trans('general.licenses'), trans('general.accessories'), trans('general.consumables'), - trans('admin/users/table.groups'), + trans('general.groups'), trans('general.notes'), trans('admin/users/table.activated'), trans('general.created_at'), @@ -626,7 +619,11 @@ class UsersController extends Controller public function printInventory($id) { $this->authorize('view', User::class); - $show_user = User::where('id', $id)->withTrashed()->first(); + $show_user = Company::scopeCompanyables(User::where('id', $id)->withTrashed()->first()); + + // Make sure they can view this particular user + $this->authorize('view', $show_user); + $assets = Asset::where('assigned_to', $id)->where('assigned_type', User::class)->with('model', 'model.category')->get(); $accessories = $show_user->accessories()->get(); $consumables = $show_user->consumables()->get(); @@ -651,16 +648,23 @@ class UsersController extends Controller { $this->authorize('view', User::class); - if (!$user = User::find($id)) { - return redirect()->back() - ->with('error', trans('admin/users/message.user_not_found', ['id' => $id])); - } - if (empty($user->email)) { - return redirect()->back()->with('error', trans('admin/users/message.user_has_no_email')); + $user = Company::scopeCompanyables(User::find($id)); + + // Make sure they can view this particular user + $this->authorize('view', $user); + + if ($user) { + + if (empty($user->email)) { + return redirect()->back()->with('error', trans('admin/users/message.user_has_no_email')); + } + + $user->notify((new CurrentInventory($user))); + return redirect()->back()->with('success', trans('admin/users/general.user_notified')); } - $user->notify((new CurrentInventory($user))); - return redirect()->back()->with('success', trans('admin/users/general.user_notified')); + return redirect()->back()->with('error', trans('admin/users/message.user_not_found', ['id' => $id])); + } /** @@ -672,19 +676,19 @@ class UsersController extends Controller */ public function sendPasswordReset($id) { - if (($user = User::find($id)) && ($user->activated == '1') && ($user->email != '') && ($user->ldap_import == '0')) { + if (($user = Company::scopeCompanyables(User::find($id))) && ($user->activated == '1') && ($user->email != '') && ($user->ldap_import == '0')) { $credentials = ['email' => trim($user->email)]; try { Password::sendResetLink($credentials); - return redirect()->back()->with('success', trans('admin/users/message.password_reset_sent', ['email' => $user->email])); + } catch (\Exception $e) { - return redirect()->back()->with('error', ' Error sending email. :( '); + return redirect()->back()->with('error', trans('general.error_sending_email')); } } - return redirect()->back()->with('error', 'User is not activated, is LDAP synced, or does not have an email address '); + return redirect()->back()->with('error', trans('general.pwd_reset_not_sent')); } } diff --git a/app/Http/Requests/ImageUploadRequest.php b/app/Http/Requests/ImageUploadRequest.php index 25156181e9..9677111059 100644 --- a/app/Http/Requests/ImageUploadRequest.php +++ b/app/Http/Requests/ImageUploadRequest.php @@ -36,6 +36,7 @@ class ImageUploadRequest extends Request return [ 'image' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp,avif', 'avatar' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp,avif', + 'favicon' => 'mimes:png,gif,jpg,jpeg,svg,bmp,svg+xml,webp,image/x-icon,image/vnd.microsoft.icon,ico', ]; } @@ -103,9 +104,9 @@ class ImageUploadRequest extends Request \Log::info('File name will be: '.$file_name); \Log::debug('File extension is: '.$ext); - if (($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) { - // If the file is a webp or avif, we need to just move it since webp support - // needs to be compiled into gd for resizing to be available + if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) { + // If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing + // icons or avif, and webp support and needs to be compiled into gd for resizing to be available Storage::disk('public')->put($path.'/'.$file_name, file_get_contents($image)); } elseif($image->getMimeType() == 'image/svg+xml') { diff --git a/app/Models/Company.php b/app/Models/Company.php index 60a8022ed7..4b718b4200 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -81,26 +81,6 @@ final class Company extends SnipeModel } } - /** - * Scoping table queries, determining if a logged in user is part of a company, and only allows - * that user to see items associated with that company - */ - private static function scopeCompanyablesDirectly($query, $column = 'company_id', $table_name = null) - { - if (Auth::user()) { - $company_id = Auth::user()->company_id; - } else { - $company_id = null; - } - - $table = ($table_name) ? $table_name."." : $query->getModel()->getTable()."."; - - if (\Schema::hasColumn($query->getModel()->getTable(), $column)) { - return $query->where($table.$column, '=', $company_id); - } else { - return $query->join('users as users_comp', 'users_comp.id', 'user_id')->where('users_comp.company_id', '=', $company_id); - } - } public static function getIdFromInput($unescaped_input) { @@ -141,25 +121,49 @@ final class Company extends SnipeModel } } + /** + * Check to see if the current user should have access to the model. + * I hate this method and I think it should be refactored. + * + * @param $companyable + * @return bool|void + */ public static function isCurrentUserHasAccess($companyable) { + // When would this even happen tho?? if (is_null($companyable)) { return false; - } elseif (! static::isFullMultipleCompanySupportEnabled()) { - return true; - } elseif (!$companyable instanceof Company && !\Schema::hasColumn($companyable->getModel()->getTable(), 'company_id')) { - // This is primary for the gate:allows-check in location->isDeletable() - // Locations don't have a company_id so without this it isn't possible to delete locations with FullMultipleCompanySupport enabled - // because this function is called by SnipePermissionsPolicy->before() - return true; - } else { - if (Auth::user()) { - $current_user_company_id = Auth::user()->company_id; - $companyable_company_id = $companyable->company_id; + } - return $current_user_company_id == null || $current_user_company_id == $companyable_company_id || Auth::user()->isSuperUser(); + // If FMCS is not enabled, everyone has access, return true + if (! static::isFullMultipleCompanySupportEnabled()) { + return true; + } + + // Again, where would this happen? But check that $companyable is not a string + if (!is_string($companyable)) { + $company_table = $companyable->getModel()->getTable(); + try { + // This is primary for the gate:allows-check in location->isDeletable() + // Locations don't have a company_id so without this it isn't possible to delete locations with FullMultipleCompanySupport enabled + // because this function is called by SnipePermissionsPolicy->before() + if (!$companyable instanceof Company && !\Schema::hasColumn($company_table, 'company_id')) { + return true; + } + + } catch (\Exception $e) { + \Log::warning($e); } } + + + if (Auth::user()) { + \Log::warning('Companyable is '.$companyable); + $current_user_company_id = Auth::user()->company_id; + $companyable_company_id = $companyable->company_id; + return $current_user_company_id == null || $current_user_company_id == $companyable_company_id || Auth::user()->isSuperUser(); + } + } public static function isCurrentUserAuthorized() @@ -190,6 +194,10 @@ final class Company extends SnipeModel && ($this->users()->count() === 0); } + /** + * @param $unescaped_input + * @return int|mixed|string|null + */ public static function getIdForUser($unescaped_input) { if (! static::isFullMultipleCompanySupportEnabled() || Auth::user()->isSuperUser()) { @@ -199,38 +207,6 @@ final class Company extends SnipeModel } } - public static function scopeCompanyables($query, $column = 'company_id', $table_name = null) - { - // If not logged in and hitting this, assume we are on the command line and don't scope?' - if (! static::isFullMultipleCompanySupportEnabled() || (Auth::check() && Auth::user()->isSuperUser()) || (! Auth::check())) { - return $query; - } else { - return static::scopeCompanyablesDirectly($query, $column, $table_name); - } - } - - public static function scopeCompanyableChildren(array $companyable_names, $query) - { - if (count($companyable_names) == 0) { - throw new Exception('No Companyable Children to scope'); - } elseif (! static::isFullMultipleCompanySupportEnabled() || (Auth::check() && Auth::user()->isSuperUser())) { - return $query; - } else { - $f = function ($q) { - static::scopeCompanyablesDirectly($q); - }; - - $q = $query->where(function ($q) use ($companyable_names, $f) { - $q2 = $q->whereHas($companyable_names[0], $f); - - for ($i = 1; $i < count($companyable_names); $i++) { - $q2 = $q2->orWhereHas($companyable_names[$i], $f); - } - }); - - return $q; - } - } public function users() { @@ -261,4 +237,98 @@ final class Company extends SnipeModel { return $this->hasMany(Component::class, 'company_id'); } + + /** + * START COMPANY SCOPING FOR FMCS + */ + + /** + * Scoping table queries, determining if a logged in user is part of a company, and only allows the user to access items associated with that company if FMCS is enabled. + * + * This method is the one that the CompanyableTrait uses to contrain queries automatically, however that trait CANNOT be + * applied to the user's model, since it causes an infinite loop against the authenticated user. + * + * @todo - refactor that trait to handle the user's model as well. + * + * @author [A. Gianotto] + * @param $query + * @param $column + * @param $table_name + * @return mixed + */ + public static function scopeCompanyables($query, $column = 'company_id', $table_name = null) + { + // If not logged in and hitting this, assume we are on the command line and don't scope?' + if (! static::isFullMultipleCompanySupportEnabled() || (Auth::check() && Auth::user()->isSuperUser()) || (! Auth::check())) { + return $query; + } else { + \Log::debug('Fire scopeCompanyablesDirectly.'); + return static::scopeCompanyablesDirectly($query, $column, $table_name); + } + } + + /** + * Scoping table queries, determining if a logged in user is part of a company, and only allows + * that user to see items associated with that company + */ + private static function scopeCompanyablesDirectly($query, $column = 'company_id', $table_name = null) + { + // Get the company ID of the logged in user, or set it to null if there is no company assicoated with the user + if (Auth::user()) { + \Log::debug('Admin company is: '.Auth::user()->company_id); + $company_id = Auth::user()->company_id; + } else { + $company_id = null; + } + + // Dynamically get the table name if it's not passed in, based on the model we're querying against + $table = ($table_name) ? $table_name."." : $query->getModel()->getTable()."."; + \Log::debug('Model is: '.$query->getModel()); + + \Log::debug('Table is: '.$table); + + // If the column exists in the table, use it to scope the query + if (\Schema::hasColumn($query->getModel()->getTable(), $column)) { + return $query->where($table.$column, '=', $company_id); + } else { + return $query->join('users as users_comp', 'users_comp.id', 'user_id')->where('users_comp.company_id', '=', $company_id); + } + } + + /** + * I legit do not know what this method does, but we can't remove it (yet). + * + * This gets invoked by CompanyableChildScope, but I'm not sure what it does. + * + * @author [A. Gianotto] + * @param array $companyable_names + * @param $query + * @return mixed + */ + public static function scopeCompanyableChildren(array $companyable_names, $query) + { + \Log::debug('Company Names in scopeCompanyableChildren: '.print_r($companyable_names, true)); + + if (count($companyable_names) == 0) { + throw new Exception('No Companyable Children to scope'); + } elseif (! static::isFullMultipleCompanySupportEnabled() || (Auth::check() && Auth::user()->isSuperUser())) { + return $query; + } else { + $f = function ($q) { + \Log::debug('scopeCompanyablesDirectly firing '); + static::scopeCompanyablesDirectly($q); + }; + + $q = $query->where(function ($q) use ($companyable_names, $f) { + $q2 = $q->whereHas($companyable_names[0], $f); + + for ($i = 1; $i < count($companyable_names); $i++) { + $q2 = $q2->orWhereHas($companyable_names[$i], $f); + } + }); + + return $q; + } + } + } diff --git a/app/Models/CompanyableTrait.php b/app/Models/CompanyableTrait.php index b03b346d2e..df67f2be4f 100644 --- a/app/Models/CompanyableTrait.php +++ b/app/Models/CompanyableTrait.php @@ -5,8 +5,13 @@ namespace App\Models; trait CompanyableTrait { /** - * Boot the companyable trait for a model. + * This trait is used to scope models to the current company. To use this scope on companyable models, + * we use the "use Companyable;" statement at the top of the mode. * + * We CANNOT USE THIS ON USERS, as it causes an infinite loop and prevents users from logging in, since this scope will be + * applied to the currently logged in (or logging in) user in addition to the user model for viewing lists of users. + * + * @see \App\Models\Company\Company::scopeCompanyables() * @return void */ public static function bootCompanyableTrait() diff --git a/app/Models/Labels/FieldOption.php b/app/Models/Labels/FieldOption.php index 7e45cc0ce7..38a90e31dc 100644 --- a/app/Models/Labels/FieldOption.php +++ b/app/Models/Labels/FieldOption.php @@ -18,8 +18,14 @@ class FieldOption { // assignedTo directly on the asset is a special case where // we want to avoid returning the property directly // and instead return the entity's presented name. - if ($dataPath[0] === 'assignedTo'){ - return $asset->assignedTo ? $asset->assignedTo->present()->fullName() : null; + if ($dataPath[0] === 'assignedTo') { + if ($asset->relationLoaded('assignedTo')) { + // If the "assignedTo" relationship was eager loaded then the way to get the + // relationship changes from $asset->assignedTo to $asset->assigned. + return $asset->assigned ? $asset->assigned->present()->fullName() : null; + } + + return $asset->assignedTo ? $asset->assignedTo->present()->fullName() : null; } return $dataPath->reduce(function ($myValue, $path) { diff --git a/app/Models/License.php b/app/Models/License.php index 57a46336a5..8ed68990c9 100755 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -81,6 +81,7 @@ class License extends Depreciable 'serial', 'supplier_id', 'termination_date', + 'free_seat_count', 'user_id', 'min_amt', ]; @@ -114,6 +115,7 @@ class License extends Depreciable 'category' => ['name'], 'depreciation' => ['name'], ]; + protected $appends = ['free_seat_count']; /** * Update seat counts when the license is updated @@ -280,6 +282,16 @@ class License extends Depreciable } $this->attributes['termination_date'] = $value; } + /** + * Sets free_seat_count attribute + * + * @author G. Martinez + * @since [v6.3] + * @return mixed + */ + public function getFreeSeatCountAttribute(){ + return $this->attributes['free_seat_count'] = $this->remaincount(); + } /** * Establishes the license -> company relationship @@ -502,7 +514,13 @@ class License extends Depreciable ->whereNull('deleted_at') ->count(); } - + /** + * Returns the available seats remaining + * + * @author A. Gianotto + * @since [v2.0] + * @return int + */ /** * Returns the number of total available seats for this license @@ -579,7 +597,7 @@ class License extends Depreciable $taken = $this->assigned_seats_count; $diff = ($total - $taken); - return $diff; + return (int) $diff; } /** diff --git a/app/Policies/SnipePermissionsPolicy.php b/app/Policies/SnipePermissionsPolicy.php index d4f2d88ccd..96c94cd776 100644 --- a/app/Policies/SnipePermissionsPolicy.php +++ b/app/Policies/SnipePermissionsPolicy.php @@ -35,16 +35,50 @@ abstract class SnipePermissionsPolicy public function before(User $user, $ability, $item) { - // Lets move all company related checks here. - if ($item instanceof \App\Models\SnipeModel && ! Company::isCurrentUserHasAccess($item)) { - return false; - } - // If an admin, they can do all asset related tasks. + /** + * If an admin, they can do all item related tasks, but ARE constrained by FMCSA company access. + * That scoping happens on the model level (except for the Users model) via the Companyable trait. + * + * This does lead to some inconsistencies in the responses, since attempting to edit assets, + * accessories, etc (anything other than users) will result in a Forbidden error, whereas the users + * area will redirect with "That user doesn't exist" since the scoping is handled directly on those queries. + * + * The *superuser* global permission gets handled in the AuthServiceProvider before() method. + * + * @see https://snipe-it.readme.io/docs/permissions + */ + if ($user->hasAccess('admin')) { return true; } + + /** + * If we got here by $this→authorize('something', $actualModel) then we can continue on Il but if we got here + * via $this→authorize('something', Model::class) then calling Company:: isCurrentUserHasAccess($item) gets weird. + * Bail out here by returning "nothing" and allow the relevant method lower in this class to be called and handle authorization. + */ + if (!$item instanceof Model){ + return; + } + + + /** + * The Company::isCurrentUserHasAccess() method from the company model handles the check for FMCS already so we + * don't have to do that here. + */ + if (!Company::isCurrentUserHasAccess($item)) { + return false; + } + } + + /** + * These methods handle the generic view/create/edit/delete permissions for the model. + * + * @param User $user + * @return bool + */ public function index(User $user) { return $user->hasAccess($this->columnName().'.view'); diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index d14e5738d4..51e6858c9c 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -93,21 +93,28 @@ class AuthServiceProvider extends ServiceProvider Passport::personalAccessTokensExpireIn(Carbon::now()->addYears(config('passport.expiration_years'))); Passport::withCookieSerialization(); - // -------------------------------- - // BEFORE ANYTHING ELSE - // -------------------------------- - // If this condition is true, ANYTHING else below will be assumed - // to be true. This can cause weird blade behavior. + + /** + * BEFORE ANYTHING ELSE + * + * If this condition is true, ANYTHING else below will be assumed to be true. + * This is where we set the superadmin permission to allow superadmins to be able to do everything within the system. + * + */ Gate::before(function ($user) { if ($user->isSuperUser()) { return true; } }); - // -------------------------------- - // GENERAL GATES - // These control general sections of the admin - // -------------------------------- + + /** + * GENERAL GATES + * + * These control general sections of the admin. These definitions are used in our blades via @can('blah) and also + * use in our controllers to determine if a user has access to a certain area. + */ + Gate::define('admin', function ($user) { if ($user->hasAccess('admin')) { return true; diff --git a/app/Providers/ValidationServiceProvider.php b/app/Providers/ValidationServiceProvider.php index 803d540865..4646b3c78b 100644 --- a/app/Providers/ValidationServiceProvider.php +++ b/app/Providers/ValidationServiceProvider.php @@ -279,7 +279,24 @@ class ValidationServiceProvider extends ServiceProvider Validator::extend('is_unique_department', function ($attribute, $value, $parameters, $validator) { $data = $validator->getData(); - if ((array_key_exists('location_id', $data) && $data['location_id'] != null) && (array_key_exists('company_id', $data) && $data['company_id'] != null)) { + + if ( + array_key_exists('location_id', $data) && $data['location_id'] !== null && + array_key_exists('company_id', $data) && $data['company_id'] !== null + ) { + //for updating existing departments + if(array_key_exists('id', $data) && $data['id'] !== null){ + $count = Department::where('name', $data['name']) + ->where('location_id', $data['location_id']) + ->where('company_id', $data['company_id']) + ->whereNotNull('company_id') + ->whereNotNull('location_id') + ->where('id', '!=', $data['id']) + ->count('name'); + + return $count < 1; + }else // for entering in new departments + { $count = Department::where('name', $data['name']) ->where('location_id', $data['location_id']) ->where('company_id', $data['company_id']) @@ -289,9 +306,10 @@ class ValidationServiceProvider extends ServiceProvider return $count < 1; } + } else { return true; - } + } }); Validator::extend('not_array', function ($attribute, $value, $parameters, $validator) { diff --git a/app/View/Label.php b/app/View/Label.php index f47ad6acd5..3ec3a4099c 100644 --- a/app/View/Label.php +++ b/app/View/Label.php @@ -142,7 +142,32 @@ class Label implements View // Remove Duplicates $toAdd = $field ->filter(fn($o) => !$myFields->contains('dataSource', $o['dataSource'])) - ->first(); + // For fields that have multiple options, we need to combine them + // into a single field so all values are displayed. + ->reduce(function ($previous, $current) { + // On the first iteration we simply return the item. + // If there is only one item to be processed for the row + // then this effectively skips everything below this if block. + if (is_null($previous)) { + return $current; + } + + // At this point we are dealing with a row with multiple items being displayed. + // We need to combine the label and value of the current item with the previous item. + + // The end result of this will be in this format: + // {labelOne} {valueOne} | {labelTwo} {valueTwo} | {labelThree} {valueThree} + $previous['value'] = trim(implode(' | ', [ + implode(' ', [$previous['label'], $previous['value']]), + implode(' ', [$current['label'], $current['value']]), + ])); + + // We'll set the label to an empty string since we + // injected the label into the value field above. + $previous['label'] = ''; + + return $previous; + }); return $toAdd ? $myFields->push($toAdd) : $myFields; }, new Collection()); diff --git a/config/database.php b/config/database.php index 1b4feeca93..de3c40bb25 100755 --- a/config/database.php +++ b/config/database.php @@ -96,6 +96,7 @@ return [ PDO::MYSQL_ATTR_SSL_CERT => env('DB_SSL_CERT_PATH'), // /path/to/cert.pem PDO::MYSQL_ATTR_SSL_CA => env('DB_SSL_CA_PATH'), // /path/to/ca.pem PDO::MYSQL_ATTR_SSL_CIPHER => env('DB_SSL_CIPHER'), + PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => env('DB_SSL_VERIFY_SERVER'), //true/false ]) : [], ], diff --git a/config/version.php b/config/version.php index 9eb8953058..bb750c1364 100644 --- a/config/version.php +++ b/config/version.php @@ -1,10 +1,10 @@ 'v6.3.4', - 'full_app_version' => 'v6.3.4 - build 13139-g6f9ba6ede', - 'build_version' => '13139', + 'full_app_version' => 'v6.3.4 - build 13226-g5229dd65c', + 'build_version' => '13226', 'prerelease_version' => '', - 'hash_version' => 'g6f9ba6ede', - 'full_hash' => 'v6.3.4-234-g6f9ba6ede', + 'hash_version' => 'g5229dd65c', + 'full_hash' => 'v6.3.4-85-g5229dd65c', 'branch' => 'develop', ); \ No newline at end of file diff --git a/database/factories/AssetFactory.php b/database/factories/AssetFactory.php index 5461303c88..6c5e883636 100644 --- a/database/factories/AssetFactory.php +++ b/database/factories/AssetFactory.php @@ -4,6 +4,7 @@ namespace Database\Factories; use App\Models\Asset; use App\Models\AssetModel; +use App\Models\CustomField; use App\Models\Location; use App\Models\Statuslabel; use App\Models\Supplier; @@ -353,6 +354,16 @@ class AssetFactory extends Factory return $this->state(['requestable' => false]); } + public function hasEncryptedCustomField(CustomField $field = null) + { + return $this->state(function () use ($field) { + return [ + 'model_id' => AssetModel::factory()->hasEncryptedCustomField($field), + ]; + }); + } + + /** * This allows bypassing model level validation if you want to purposefully * create an asset in an invalid state. Validation is turned back on diff --git a/database/factories/AssetModelFactory.php b/database/factories/AssetModelFactory.php index 4881d6560b..ed3d478261 100644 --- a/database/factories/AssetModelFactory.php +++ b/database/factories/AssetModelFactory.php @@ -3,6 +3,7 @@ namespace Database\Factories; use App\Models\AssetModel; +use App\Models\CustomField; use App\Models\CustomFieldset; use App\Models\Depreciation; use App\Models\Manufacturer; @@ -429,4 +430,13 @@ class AssetModelFactory extends Factory ]; }); } + + public function hasEncryptedCustomField(CustomField $field = null) + { + return $this->state(function () use ($field) { + return [ + 'fieldset_id' => CustomFieldset::factory()->hasEncryptedCustomField($field), + ]; + }); + } } diff --git a/database/factories/CustomFieldsetFactory.php b/database/factories/CustomFieldsetFactory.php index e651b5c8d3..9a410ba25f 100644 --- a/database/factories/CustomFieldsetFactory.php +++ b/database/factories/CustomFieldsetFactory.php @@ -3,6 +3,7 @@ namespace Database\Factories; use App\Models\CustomFieldset; +use App\Models\CustomField; use Illuminate\Database\Eloquent\Factories\Factory; class CustomFieldsetFactory extends Factory @@ -43,4 +44,13 @@ class CustomFieldsetFactory extends Factory ]; }); } + + public function hasEncryptedCustomField(CustomField $field = null) + { + return $this->afterCreating(function (CustomFieldset $fieldset) use ($field) { + $field = $field ?? CustomField::factory()->testEncrypted()->create(); + + $fieldset->fields()->attach($field, ['order' => '1', 'required' => false]); + }); + } } diff --git a/package-lock.json b/package-lock.json index fb02c5139e..f5e98c2f6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "papaparse": "^4.3.3", "select2": "4.0.13", "sheetjs": "^2.0.0", + "signature_pad": "^4.2.0", "tableexport.jquery.plugin": "1.28.0", "tether": "^1.4.0", "webpack": "^5.90.2" @@ -11188,6 +11189,11 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/signature_pad": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-4.2.0.tgz", + "integrity": "sha512-YLWysmaUBaC5wosAKkgbX7XI+LBv2w5L0QUcI6Jc4moHYzv9BUBJtAyNLpWzHjtjKTeWOH6bfP4a4pzf0UinfQ==" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", diff --git a/package.json b/package.json index 3454c7e16c..b893b053c2 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "papaparse": "^4.3.3", "select2": "4.0.13", "sheetjs": "^2.0.0", + "signature_pad": "^4.2.0", "tableexport.jquery.plugin": "1.28.0", "tether": "^1.4.0", "webpack": "^5.90.2" diff --git a/public/css/build/app.css b/public/css/build/app.css index 346d17981e..0be53fb0c6 100644 Binary files a/public/css/build/app.css and b/public/css/build/app.css differ diff --git a/public/css/build/overrides.css b/public/css/build/overrides.css index 867998daad..6d13a6bf49 100644 Binary files a/public/css/build/overrides.css and b/public/css/build/overrides.css differ diff --git a/public/css/dist/all.css b/public/css/dist/all.css index b294ed2646..41255d8523 100644 Binary files a/public/css/dist/all.css and b/public/css/dist/all.css differ diff --git a/public/css/dist/skins/skin-black-dark.css b/public/css/dist/skins/skin-black-dark.css index bdab8393c5..4f3102b8a3 100644 Binary files a/public/css/dist/skins/skin-black-dark.css and b/public/css/dist/skins/skin-black-dark.css differ diff --git a/public/css/dist/skins/skin-black-dark.min.css b/public/css/dist/skins/skin-black-dark.min.css index fa7c210124..1ad2aa8a69 100644 Binary files a/public/css/dist/skins/skin-black-dark.min.css and b/public/css/dist/skins/skin-black-dark.min.css differ diff --git a/public/css/dist/skins/skin-black.css b/public/css/dist/skins/skin-black.css index 2d3d0edc3b..175fc739e9 100644 Binary files a/public/css/dist/skins/skin-black.css and b/public/css/dist/skins/skin-black.css differ diff --git a/public/css/dist/skins/skin-black.min.css b/public/css/dist/skins/skin-black.min.css index 5a5decc906..7e315d8595 100644 Binary files a/public/css/dist/skins/skin-black.min.css and b/public/css/dist/skins/skin-black.min.css differ diff --git a/public/css/dist/skins/skin-blue-dark.css b/public/css/dist/skins/skin-blue-dark.css index 9c1e130515..ecddf9e16a 100644 Binary files a/public/css/dist/skins/skin-blue-dark.css and b/public/css/dist/skins/skin-blue-dark.css differ diff --git a/public/css/dist/skins/skin-blue-dark.min.css b/public/css/dist/skins/skin-blue-dark.min.css index a70912484e..f5f42196e2 100644 Binary files a/public/css/dist/skins/skin-blue-dark.min.css and b/public/css/dist/skins/skin-blue-dark.min.css differ diff --git a/public/css/dist/skins/skin-blue.css b/public/css/dist/skins/skin-blue.css index cdc62766ec..a27a6bb773 100644 Binary files a/public/css/dist/skins/skin-blue.css and b/public/css/dist/skins/skin-blue.css differ diff --git a/public/css/dist/skins/skin-blue.min.css b/public/css/dist/skins/skin-blue.min.css index 09b0bba755..cdaf93a5ce 100644 Binary files a/public/css/dist/skins/skin-blue.min.css and b/public/css/dist/skins/skin-blue.min.css differ diff --git a/public/css/dist/skins/skin-contrast.css b/public/css/dist/skins/skin-contrast.css index 8f5c99c013..741b70b4db 100644 Binary files a/public/css/dist/skins/skin-contrast.css and b/public/css/dist/skins/skin-contrast.css differ diff --git a/public/css/dist/skins/skin-contrast.min.css b/public/css/dist/skins/skin-contrast.min.css index 25ebe1f277..a4c34ffbc1 100644 Binary files a/public/css/dist/skins/skin-contrast.min.css and b/public/css/dist/skins/skin-contrast.min.css differ diff --git a/public/css/dist/skins/skin-green-dark.css b/public/css/dist/skins/skin-green-dark.css index 7c035a9ec9..333f1e505d 100644 Binary files a/public/css/dist/skins/skin-green-dark.css and b/public/css/dist/skins/skin-green-dark.css differ diff --git a/public/css/dist/skins/skin-green-dark.min.css b/public/css/dist/skins/skin-green-dark.min.css index 12bcb66477..0ac36246c9 100644 Binary files a/public/css/dist/skins/skin-green-dark.min.css and b/public/css/dist/skins/skin-green-dark.min.css differ diff --git a/public/css/dist/skins/skin-green.css b/public/css/dist/skins/skin-green.css index cd113d5e54..8808f60bc4 100644 Binary files a/public/css/dist/skins/skin-green.css and b/public/css/dist/skins/skin-green.css differ diff --git a/public/css/dist/skins/skin-green.min.css b/public/css/dist/skins/skin-green.min.css index 620d48e3f4..e83afedd43 100644 Binary files a/public/css/dist/skins/skin-green.min.css and b/public/css/dist/skins/skin-green.min.css differ diff --git a/public/css/dist/skins/skin-orange-dark.css b/public/css/dist/skins/skin-orange-dark.css index b52b17dd02..67b4288bf8 100644 Binary files a/public/css/dist/skins/skin-orange-dark.css and b/public/css/dist/skins/skin-orange-dark.css differ diff --git a/public/css/dist/skins/skin-orange-dark.min.css b/public/css/dist/skins/skin-orange-dark.min.css index 8c0492a9b5..da4eaf5589 100644 Binary files a/public/css/dist/skins/skin-orange-dark.min.css and b/public/css/dist/skins/skin-orange-dark.min.css differ diff --git a/public/css/dist/skins/skin-orange.css b/public/css/dist/skins/skin-orange.css index 411d4994ac..530e5d545b 100644 Binary files a/public/css/dist/skins/skin-orange.css and b/public/css/dist/skins/skin-orange.css differ diff --git a/public/css/dist/skins/skin-orange.min.css b/public/css/dist/skins/skin-orange.min.css index af01a70cff..3b6dc59701 100644 Binary files a/public/css/dist/skins/skin-orange.min.css and b/public/css/dist/skins/skin-orange.min.css differ diff --git a/public/css/dist/skins/skin-purple-dark.css b/public/css/dist/skins/skin-purple-dark.css index 33a21545b3..3d313f695b 100644 Binary files a/public/css/dist/skins/skin-purple-dark.css and b/public/css/dist/skins/skin-purple-dark.css differ diff --git a/public/css/dist/skins/skin-purple-dark.min.css b/public/css/dist/skins/skin-purple-dark.min.css index 0c0a5bd957..1186059bf9 100644 Binary files a/public/css/dist/skins/skin-purple-dark.min.css and b/public/css/dist/skins/skin-purple-dark.min.css differ diff --git a/public/css/dist/skins/skin-purple.css b/public/css/dist/skins/skin-purple.css index 344cbfce10..1b2f950a12 100644 Binary files a/public/css/dist/skins/skin-purple.css and b/public/css/dist/skins/skin-purple.css differ diff --git a/public/css/dist/skins/skin-purple.min.css b/public/css/dist/skins/skin-purple.min.css index 8a89eae1a6..1bdfc63204 100644 Binary files a/public/css/dist/skins/skin-purple.min.css and b/public/css/dist/skins/skin-purple.min.css differ diff --git a/public/css/dist/skins/skin-red-dark.css b/public/css/dist/skins/skin-red-dark.css index b92ad70dfa..f847548492 100644 Binary files a/public/css/dist/skins/skin-red-dark.css and b/public/css/dist/skins/skin-red-dark.css differ diff --git a/public/css/dist/skins/skin-red-dark.min.css b/public/css/dist/skins/skin-red-dark.min.css index afe056285f..2cbdb935df 100644 Binary files a/public/css/dist/skins/skin-red-dark.min.css and b/public/css/dist/skins/skin-red-dark.min.css differ diff --git a/public/css/dist/skins/skin-red.css b/public/css/dist/skins/skin-red.css index c6167ad739..2bd78e3698 100644 Binary files a/public/css/dist/skins/skin-red.css and b/public/css/dist/skins/skin-red.css differ diff --git a/public/css/dist/skins/skin-red.min.css b/public/css/dist/skins/skin-red.min.css index 4f73db5918..4a1d68315e 100644 Binary files a/public/css/dist/skins/skin-red.min.css and b/public/css/dist/skins/skin-red.min.css differ diff --git a/public/css/dist/skins/skin-yellow-dark.css b/public/css/dist/skins/skin-yellow-dark.css index aea34738e2..8df93cc8ee 100644 Binary files a/public/css/dist/skins/skin-yellow-dark.css and b/public/css/dist/skins/skin-yellow-dark.css differ diff --git a/public/css/dist/skins/skin-yellow-dark.min.css b/public/css/dist/skins/skin-yellow-dark.min.css index 73b2bba0df..39d833d598 100644 Binary files a/public/css/dist/skins/skin-yellow-dark.min.css and b/public/css/dist/skins/skin-yellow-dark.min.css differ diff --git a/public/css/dist/skins/skin-yellow.css b/public/css/dist/skins/skin-yellow.css index 5424b7f75d..aaafaa2faf 100644 Binary files a/public/css/dist/skins/skin-yellow.css and b/public/css/dist/skins/skin-yellow.css differ diff --git a/public/css/dist/skins/skin-yellow.min.css b/public/css/dist/skins/skin-yellow.min.css index f64159f334..0d12ea85cf 100644 Binary files a/public/css/dist/skins/skin-yellow.min.css and b/public/css/dist/skins/skin-yellow.min.css differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index fe0f7f3c27..eb676d0af6 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -1,24 +1,24 @@ { "/js/build/app.js": "/js/build/app.js?id=a05df3d0d95cb1cb86b26e858563009f", - "/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=b9a74ec0cd68f83e7480d5ae39919beb", - "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=392cc93cfc0be0349bab9697669dd091", - "/css/build/overrides.css": "/css/build/overrides.css?id=5276161c4e09d905ece0419d16151cbf", - "/css/build/app.css": "/css/build/app.css?id=dde7b2ff6869386f242010a9056c9705", + "/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=fbe33c09aabd4c4441bd0c7df51dae27", + "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=81c1f603db10885e65c582b6a4d9990a", + "/css/build/overrides.css": "/css/build/overrides.css?id=33450d0bef7d27f0f626ba360088c00c", + "/css/build/app.css": "/css/build/app.css?id=2b8033f07b6bddad5ab3c2cf944e2092", "/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=a67bd93bed52e6a29967fe472de66d6c", - "/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=268041e902b019730c23ee3875838005", - "/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=d409d9b1a3b69247df8b98941ba06e33", - "/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=4a9e8c5e7b09506fa3e3a3f42849e07f", - "/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=21fef066e0bb1b02fd83fcb6694fad5f", - "/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=fc7adb943668ac69fe4b646625a7571f", - "/css/dist/skins/skin-purple-dark.css": "/css/dist/skins/skin-purple-dark.css?id=9f944e8021781af1ce45d27765d1c0c2", - "/css/dist/skins/skin-purple.css": "/css/dist/skins/skin-purple.css?id=cf6c8c340420724b02d6e787ef9bded5", - "/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=6cf460bed48ab738041f60231a3f005a", - "/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=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/all.css": "/css/dist/all.css?id=bd339274b6efdcb815615927d1e734cd", + "/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=ff8ee9591f9689de2ceff34df3abf693", + "/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=e1a8f357e3f9f2fe2ca3aa73028d7660", + "/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=ac9d06b3a2273c86e96f3d518556dcc6", + "/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=9d579e001f4d9f61a63c801ee2da1570", + "/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=eaa06b4be5365eeb47df9d78dc8d3f7b", + "/css/dist/skins/skin-purple-dark.css": "/css/dist/skins/skin-purple-dark.css?id=9dc2632270ea725008c979ba0c3e5e6e", + "/css/dist/skins/skin-purple.css": "/css/dist/skins/skin-purple.css?id=51e57ecb88994262fd444f8805ef8dc5", + "/css/dist/skins/skin-red-dark.css": "/css/dist/skins/skin-red-dark.css?id=164148ec02f4c537f70cc1390ea35fca", + "/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=f67a07ab14cd87fe782aa8b1558dbe51", + "/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=875c9e6fdb26015a4eaad7c34abeba3d", + "/css/dist/skins/skin-green-dark.css": "/css/dist/skins/skin-green-dark.css?id=a9cf77c9b9a7b1c9be035df00294d453", + "/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=2c79e570b79bae4d510cce7e99032359", + "/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=dc03c84f501f028349540a19cff67ac5", + "/css/dist/all.css": "/css/dist/all.css?id=d950c2d932c847f4dd554dc14fb87957", "/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=69e5d8e4e818f05fd882cceb758d1eba", @@ -34,19 +34,19 @@ "/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=857da5daffd13e0553510e5ccd410c79", "/js/dist/all.js": "/js/dist/all.js?id=5f4bdd1b17a98eb4b59085823cf63972", "/js/dist/all-defer.js": "/js/dist/all-defer.js?id=19ccc62a8f1ea103dede4808837384d4", - "/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=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=6cf460bed48ab738041f60231a3f005a", - "/css/dist/skins/skin-blue.min.css": "/css/dist/skins/skin-blue.min.css?id=392cc93cfc0be0349bab9697669dd091", - "/css/dist/skins/skin-blue-dark.min.css": "/css/dist/skins/skin-blue-dark.min.css?id=4a9e8c5e7b09506fa3e3a3f42849e07f", - "/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=fc7adb943668ac69fe4b646625a7571f", - "/css/dist/skins/skin-yellow-dark.min.css": "/css/dist/skins/skin-yellow-dark.min.css?id=21fef066e0bb1b02fd83fcb6694fad5f", - "/css/dist/skins/skin-red.min.css": "/css/dist/skins/skin-red.min.css?id=b9a74ec0cd68f83e7480d5ae39919beb", - "/css/dist/skins/skin-red-dark.min.css": "/css/dist/skins/skin-red-dark.min.css?id=7f0eb9e355b36b41c61c3af3b4d41143", - "/css/dist/skins/skin-purple.min.css": "/css/dist/skins/skin-purple.min.css?id=cf6c8c340420724b02d6e787ef9bded5", - "/css/dist/skins/skin-purple-dark.min.css": "/css/dist/skins/skin-purple-dark.min.css?id=9f944e8021781af1ce45d27765d1c0c2", - "/css/dist/skins/skin-orange.min.css": "/css/dist/skins/skin-orange.min.css?id=268041e902b019730c23ee3875838005", - "/css/dist/skins/skin-orange-dark.min.css": "/css/dist/skins/skin-orange-dark.min.css?id=d409d9b1a3b69247df8b98941ba06e33", - "/css/dist/skins/skin-contrast.min.css": "/css/dist/skins/skin-contrast.min.css?id=f0fbbb0ac729ea092578fb05ca615460" + "/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=2c79e570b79bae4d510cce7e99032359", + "/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=a9cf77c9b9a7b1c9be035df00294d453", + "/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=875c9e6fdb26015a4eaad7c34abeba3d", + "/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=f67a07ab14cd87fe782aa8b1558dbe51", + "/css/dist/skins/skin-blue.min.css": "/css/dist/skins/skin-blue.min.css?id=81c1f603db10885e65c582b6a4d9990a", + "/css/dist/skins/skin-blue-dark.min.css": "/css/dist/skins/skin-blue-dark.min.css?id=ac9d06b3a2273c86e96f3d518556dcc6", + "/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=eaa06b4be5365eeb47df9d78dc8d3f7b", + "/css/dist/skins/skin-yellow-dark.min.css": "/css/dist/skins/skin-yellow-dark.min.css?id=9d579e001f4d9f61a63c801ee2da1570", + "/css/dist/skins/skin-red.min.css": "/css/dist/skins/skin-red.min.css?id=fbe33c09aabd4c4441bd0c7df51dae27", + "/css/dist/skins/skin-red-dark.min.css": "/css/dist/skins/skin-red-dark.min.css?id=164148ec02f4c537f70cc1390ea35fca", + "/css/dist/skins/skin-purple.min.css": "/css/dist/skins/skin-purple.min.css?id=51e57ecb88994262fd444f8805ef8dc5", + "/css/dist/skins/skin-purple-dark.min.css": "/css/dist/skins/skin-purple-dark.min.css?id=9dc2632270ea725008c979ba0c3e5e6e", + "/css/dist/skins/skin-orange.min.css": "/css/dist/skins/skin-orange.min.css?id=ff8ee9591f9689de2ceff34df3abf693", + "/css/dist/skins/skin-orange-dark.min.css": "/css/dist/skins/skin-orange-dark.min.css?id=e1a8f357e3f9f2fe2ca3aa73028d7660", + "/css/dist/skins/skin-contrast.min.css": "/css/dist/skins/skin-contrast.min.css?id=dc03c84f501f028349540a19cff67ac5" } diff --git a/resources/assets/less/mixins.less b/resources/assets/less/mixins.less index ff801d415e..b6ff57a4e9 100755 --- a/resources/assets/less/mixins.less +++ b/resources/assets/less/mixins.less @@ -131,7 +131,7 @@ //Hover and active states &:hover > a, &.active > a { color: @sidebar-dark-hover-color; - background: @sidebar-dark-hover-bg; + background: @link-hover-border-color; border-left-color: @link-hover-border-color; } //First Level Submenu diff --git a/resources/assets/less/overrides.less b/resources/assets/less/overrides.less index f651826ddb..69bd32a3e7 100644 --- a/resources/assets/less/overrides.less +++ b/resources/assets/less/overrides.less @@ -358,6 +358,10 @@ body { white-space: normal; } +.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading { + z-index: 0 !important; +} + @media print { a[href]:after { content: none; @@ -684,6 +688,12 @@ th.css-accessory > .th-inner::before .sidebar-menu { margin-top:100px } + .navbar-custom-menu > .navbar-nav > li.dropdown.user.user-menu { + float:right; + } + .navbar-custom-menu > .navbar-nav > li > .dropdown-menu { + margin-right:-39px; + } } @media screen and (max-width: 1268px) and (min-width: 912px){ diff --git a/resources/assets/less/skins/skin-black-dark.less b/resources/assets/less/skins/skin-black-dark.less index 6f0a2204b2..417258702b 100644 --- a/resources/assets/less/skins/skin-black-dark.less +++ b/resources/assets/less/skins/skin-black-dark.less @@ -70,7 +70,7 @@ &.btn-primary, .btn-primary:link { background-color: darken(@black, 10%); - border-color: darken(@black, 20%); + border-color: #FFF; color: #fff; } @@ -375,7 +375,10 @@ input[type=text], input[type=search] { color: var(--text-main); } .skin-black-dark .main-header .navbar .dropdown-menu li a { - color: var(--header); + color: #FFFFFF; +} +.skin-black-dark .main-header .navbar .dropdown-menu li a:hover { + background-color: #000000; } .fixed-table-body thead th .th-inner, .skin-black-dark .sidebar-menu>li.active>a, .skin-black .sidebar-menu>li:hover>a, .sidebar-toggle:hover { background-color: var(--header)!important; diff --git a/resources/assets/less/skins/skin-blue-dark.less b/resources/assets/less/skins/skin-blue-dark.less index 6c502b1781..8fedf7b82e 100644 --- a/resources/assets/less/skins/skin-blue-dark.less +++ b/resources/assets/less/skins/skin-blue-dark.less @@ -245,7 +245,6 @@ body { } .btn-primary:hover { background-color: var(--button-primary); - color: var(--link)!important; } #componentsTable>tbody>tr>td>nobr>a>i.fa { color: var(--text-main); @@ -361,7 +360,10 @@ input[type=text], input[type=search] { color: var(--text-main); } .skin-blue-dark .main-header .navbar .dropdown-menu li a { - color: var(--header); + color: #FFFFFF; +} +.skin-blue-dark .main-header .navbar .dropdown-menu li a:hover { + background-color: #3c8dbc; } .fixed-table-body thead th .th-inner, .skin-blue-dark .sidebar-menu>li.active>a, .skin-blue .sidebar-menu>li:hover>a, .sidebar-toggle:hover { background-color: var(--header)!important; diff --git a/resources/assets/less/skins/skin-green-dark.less b/resources/assets/less/skins/skin-green-dark.less index 5267803656..48616b324b 100644 --- a/resources/assets/less/skins/skin-green-dark.less +++ b/resources/assets/less/skins/skin-green-dark.less @@ -236,7 +236,6 @@ body { } .btn-primary:hover { background-color: var(--button-primary); - color: var(--link)!important; } #componentsTable>tbody>tr>td>nobr>a>i.fa { color: var(--text-main); @@ -348,7 +347,10 @@ input[type=text], input[type=search] { color: var(--text-main); } .skin-green-dark .main-header .navbar .dropdown-menu li a { - color: var(--link); + color: #FFFFFF; +} +.skin-green-dark .main-header .navbar .dropdown-menu li a:hover { + background-color: #006300; } .fixed-table-body thead th .th-inner, .skin-green-dark .sidebar-menu>li.active>a, .skin-green .sidebar-menu>li:hover>a, .sidebar-toggle:hover { background-color: var(--header)!important; diff --git a/resources/assets/less/skins/skin-orange-dark.less b/resources/assets/less/skins/skin-orange-dark.less index 98925b475e..edb35282d8 100644 --- a/resources/assets/less/skins/skin-orange-dark.less +++ b/resources/assets/less/skins/skin-orange-dark.less @@ -243,7 +243,6 @@ a:link.btn-default{ } .btn-primary:hover { background-color: var(--button-primary); - color: var(--link)!important; } #componentsTable>tbody>tr>td>nobr>a>i.fa { color: var(--text-main); @@ -359,7 +358,10 @@ input[type=text], input[type=search] { color: var(--text-main); } .skin-orange-dark .main-header .navbar .dropdown-menu li a { - color: var(--header); + color: #FFFFFF; +} +.skin-orange-dark .main-header .navbar .dropdown-menu li a:hover { + background-color: #ff8c00; } .fixed-table-body thead th .th-inner, .skin-orange-dark .sidebar-menu>li.active>a, .skin-orange .sidebar-menu>li:hover>a, .sidebar-toggle:hover { background-color: var(--header)!important; diff --git a/resources/assets/less/skins/skin-purple-dark.less b/resources/assets/less/skins/skin-purple-dark.less index 10868045af..355d6c6204 100644 --- a/resources/assets/less/skins/skin-purple-dark.less +++ b/resources/assets/less/skins/skin-purple-dark.less @@ -246,7 +246,6 @@ body { } .btn-primary:hover { background-color: var(--button-primary); - color: var(--link)!important; } #componentsTable>tbody>tr>td>nobr>a>i.fa { color: var(--text-main); @@ -362,7 +361,10 @@ input[type=text], input[type=search] { color: var(--text-main); } .skin-purple-dark .main-header .navbar .dropdown-menu li a { - color: var(--header); + color: #FFFFFF; +} +.skin-purple-dark .main-header .navbar .dropdown-menu li a:hover { + background-color: #5f5ca8; } .fixed-table-body thead th .th-inner, .skin-purple-dark .sidebar-menu>li.active>a, .skin-purple .sidebar-menu>li:hover>a, .sidebar-toggle:hover { background-color: var(--header)!important; diff --git a/resources/assets/less/skins/skin-red-dark.less b/resources/assets/less/skins/skin-red-dark.less index 3fded5f067..1bb7a91e60 100644 --- a/resources/assets/less/skins/skin-red-dark.less +++ b/resources/assets/less/skins/skin-red-dark.less @@ -247,7 +247,6 @@ body { } .btn-primary:hover { background-color: var(--button-primary); - color: var(--link)!important; } #componentsTable>tbody>tr>td>nobr>a>i.fa { color: var(--text-main); @@ -363,7 +362,10 @@ input[type=text], input[type=search] { color: var(--text-main); } .skin-red-dark .main-header .navbar .dropdown-menu li a { - color: var(--header); + color: #FFFFFF; +} +.skin-red-dark .main-header .navbar .dropdown-menu li a:hover { + background-color: #c23320; } .fixed-table-body thead th .th-inner, .skin-red-dark .sidebar-menu>li.active>a, .skin-red .sidebar-menu>li:hover>a, .sidebar-toggle:hover { background-color: var(--header)!important; diff --git a/resources/assets/less/skins/skin-yellow-dark.less b/resources/assets/less/skins/skin-yellow-dark.less index c111cb122c..7ac83a451f 100644 --- a/resources/assets/less/skins/skin-yellow-dark.less +++ b/resources/assets/less/skins/skin-yellow-dark.less @@ -55,7 +55,7 @@ &.btn-primary, .btn-primary:link { background-color: var(--button-default); - border-color: var(--button-default); + border-color: #000000; color: #545454; } @@ -355,8 +355,11 @@ input[type=text], input[type=search] { .skin-yellow-dark .main-header .navbar .dropdown-menu li a { color: var(--header); } +.skin-yellow-dark .main-header .navbar .dropdown-menu li a:hover { + background-color: #000000; +} tr th div.th-inner { - color:var(--text-main); + color: #FFFFFF; } .tab-content, .tab-pane { background-color: var(--back-main); diff --git a/resources/lang/en-US/admin/hardware/message.php b/resources/lang/en-US/admin/hardware/message.php index bf050ef974..96b4b6c949 100644 --- a/resources/lang/en-US/admin/hardware/message.php +++ b/resources/lang/en-US/admin/hardware/message.php @@ -17,6 +17,7 @@ return [ 'update' => [ 'error' => 'Asset was not updated, please try again', 'success' => 'Asset updated successfully.', + 'encrypted_warning' => 'Asset updated successfully, but encrypted custom fields were not due to permissions', 'nothing_updated' => 'No fields were selected, so nothing was updated.', 'no_assets_selected' => 'No assets were selected, so nothing was updated.', 'assets_do_not_exist_or_are_invalid' => 'Selected assets cannot be updated.', diff --git a/resources/lang/en-US/admin/licenses/message.php b/resources/lang/en-US/admin/licenses/message.php index c79f631680..27fbfe38a9 100644 --- a/resources/lang/en-US/admin/licenses/message.php +++ b/resources/lang/en-US/admin/licenses/message.php @@ -3,7 +3,7 @@ return array( 'does_not_exist' => 'License does not exist or you do not have permission to view it.', - 'user_does_not_exist' => 'User does not exist.', + 'user_does_not_exist' => 'User does not exist or you do not have permission to view them.', 'asset_does_not_exist' => 'The asset you are trying to associate with this license does not exist.', 'owner_doesnt_match_asset' => 'The asset you are trying to associate with this license is owned by somene other than the person selected in the assigned to dropdown.', '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. ', diff --git a/resources/lang/en-US/admin/licenses/table.php b/resources/lang/en-US/admin/licenses/table.php index dfce4136cb..9cabf9c883 100644 --- a/resources/lang/en-US/admin/licenses/table.php +++ b/resources/lang/en-US/admin/licenses/table.php @@ -4,6 +4,7 @@ return array( 'assigned_to' => 'Assigned To', 'checkout' => 'In/Out', + 'deleted_at' => 'Deleted at', 'id' => 'ID', 'license_email' => 'License Email', 'license_name' => 'Licensed To', diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index 9f9a0e08c7..dc44b22b2a 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -176,7 +176,7 @@ return [ 'last_name' => 'Last Name', 'license' => 'License', 'license_report' => 'License Report', - 'licenses_available' => 'licenses available', + 'licenses_available' => 'Licenses available', 'licenses' => 'Licenses', 'list_all' => 'List All', 'loading' => 'Loading... please wait....', @@ -245,6 +245,7 @@ return [ 'select_all' => 'Select All', 'search' => 'Search', 'select_category' => 'Select a Category', + 'select_datasource' => 'Select a Datasource', 'select_department' => 'Select a Department', 'select_depreciation' => 'Select a Depreciation Type', 'select_location' => 'Select a Location', @@ -507,6 +508,9 @@ return [ 'or' => 'or', 'url' => 'URL', 'edit_fieldset' => 'Edit fieldset fields and options', + 'permission_denied_superuser_demo' => 'Permission denied. You cannot update user information for superadmins on the demo.', + 'pwd_reset_not_sent' => 'User is not activated, is LDAP synced, or does not have an email address', + 'error_sending_email' => 'Error sending email', 'bulk' => [ 'delete' => [ diff --git a/resources/views/account/accept/create.blade.php b/resources/views/account/accept/create.blade.php index c05bc3a892..fa6e4b8b56 100644 --- a/resources/views/account/accept/create.blade.php +++ b/resources/views/account/accept/create.blade.php @@ -70,7 +70,7 @@

{{trans('general.sign_tos')}}

- +
@@ -94,6 +94,7 @@ @section('moar_scripts') diff --git a/resources/views/licenses/index.blade.php b/resources/views/licenses/index.blade.php index 8fa52fd1fc..82e34b1909 100755 --- a/resources/views/licenses/index.blade.php +++ b/resources/views/licenses/index.blade.php @@ -13,6 +13,9 @@ {{ trans('general.create') }} @endcan +@can('view', \App\Models\License::class) + {{ trans('general.export') }} +@endcan @stop {{-- Page content --}} diff --git a/resources/views/modals/location.blade.php b/resources/views/modals/location.blade.php index 112e710249..0b5424b294 100644 --- a/resources/views/modals/location.blade.php +++ b/resources/views/modals/location.blade.php @@ -17,7 +17,7 @@
-
+
diff --git a/resources/views/partials/label2-field-definitions.blade.php b/resources/views/partials/label2-field-definitions.blade.php index 8d8c680c94..b701ffe69a 100644 --- a/resources/views/partials/label2-field-definitions.blade.php +++ b/resources/views/partials/label2-field-definitions.blade.php @@ -306,6 +306,7 @@