Merge branch 'develop' into bug/sc-24884

This commit is contained in:
Spencer Long 2024-03-27 12:45:45 -05:00 committed by GitHub
commit 013463aafc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 600 additions and 263 deletions

View file

@ -86,6 +86,7 @@ COOKIE_DOMAIN=null
SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=15
BS_TABLE_STORAGE=cookieStorage
BS_TABLE_DEEPLINK=true
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS

View file

@ -592,6 +592,11 @@ class AssetsController extends Controller
}
}
}
if ($field->element == 'checkbox') {
if(is_array($field_val)) {
$field_val = implode(',', $field_val);
}
}
$asset->{$field->db_column} = $field_val;
@ -614,7 +619,7 @@ class AssetsController extends Controller
$asset->image = $asset->getImageUrl();
}
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.create.success')));
return response()->json(Helper::formatStandardApiResponse('success', (new AssetsTransformer)->transformAsset($asset), trans('admin/hardware/message.create.success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
@ -642,26 +647,35 @@ 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
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($request->input($field->db_column));
$asset->{$field->db_column} = Crypt::encrypt($field_val);
}
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
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;
}
}
}
@ -686,7 +700,7 @@ class AssetsController extends Controller
if ($asset->image) {
$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')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200);
}

View file

@ -45,6 +45,10 @@ class ReportsController extends Controller
$actionlogs = $actionlogs->where('action_type', '=', $request->input('action_type'))->orderBy('created_at', 'desc');
}
if ($request->filled('user_id')) {
$actionlogs = $actionlogs->where('user_id', '=', $request->input('user_id'));
}
if ($request->filled('action_source')) {
$actionlogs = $actionlogs->where('action_source', '=', $request->input('action_source'))->orderBy('created_at', 'desc');
}

View file

@ -560,7 +560,26 @@ class UsersController extends Controller
{
$this->authorize('view', User::class);
$this->authorize('view', Asset::class);
$assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model')->get();
$assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model');
// Filter on category ID
if ($request->filled('category_id')) {
$assets = $assets->InCategory($request->input('category_id'));
}
// Filter on model ID
if ($request->filled('model_id')) {
$model_ids = $request->input('model_id');
if (!is_array($model_ids)) {
$model_ids = array($model_ids);
}
$assets = $assets->InModelList($model_ids);
}
$assets = $assets->get();
return (new AssetsTransformer)->transformAssets($assets, $assets->count(), $request);
}
@ -661,7 +680,17 @@ class UsersController extends Controller
$user = User::find($request->get('id'));
$user->two_factor_secret = null;
$user->two_factor_enrolled = 0;
$user->save();
$user->saveQuietly();
// Log the reset
$logaction = new Actionlog();
$logaction->target_type = User::class;
$logaction->target_id = $user->id;
$logaction->item_type = User::class;
$logaction->item_id = $user->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = Auth::user()->id;
$logaction->logaction('2FA reset');
return response()->json(['message' => trans('admin/settings/general.two_factor_reset_success')], 200);
} catch (\Exception $e) {

View file

@ -7,6 +7,7 @@ use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CustomField;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
@ -486,11 +487,11 @@ class AssetModelsController extends Controller
* @param array $defaultValues
* @return void
*/
private function assignCustomFieldsDefaultValues(AssetModel $model, array $defaultValues)
private function assignCustomFieldsDefaultValues(AssetModel $model, array $defaultValues): bool
{
$data = array();
foreach ($defaultValues as $customFieldId => $defaultValue) {
$customField = \App\Models\CustomField::find($customFieldId);
$customField = CustomField::find($customFieldId);
$data[$customField->db_column] = $defaultValue;
}

View file

@ -102,6 +102,10 @@ class AssetsController extends Controller
{
$this->authorize(Asset::class);
// There are a lot more rules to add here but prevents
// errors around `asset_tags` not being present below.
$this->validate($request, ['asset_tags' => ['required', 'array']]);
// Handle asset tags - there could be one, or potentially many.
// This is only necessary on create, not update, since bulk editing is handled
// differently

View file

@ -260,7 +260,7 @@ class CustomFieldsController extends Controller
$field->name = trim(e($request->get("name")));
$field->element = e($request->get("element"));
$field->field_values = e($request->get("field_values"));
$field->field_values = $request->get("field_values");
$field->user_id = Auth::id();
$field->help_text = $request->get("help_text");
$field->show_in_email = $show_in_email;

View file

@ -20,6 +20,7 @@ use DB;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Image;
use Input;
use Redirect;
@ -499,6 +500,19 @@ class SettingsController extends Controller
*/
public function postSecurity(Request $request)
{
$this->validate($request, [
'pwd_secure_complexity' => 'array',
'pwd_secure_complexity.*' => [
Rule::in([
'disallow_same_pwd_as_user_fields',
'letters',
'numbers',
'symbols',
'case_diff',
])
]
]);
if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
}

View file

@ -4,6 +4,8 @@ namespace App\Http\Requests;
use App\Models\Asset;
use App\Models\Company;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Support\Facades\Gate;
class StoreAssetRequest extends ImageUploadRequest
@ -27,6 +29,8 @@ class StoreAssetRequest extends ImageUploadRequest
? Company::getIdForCurrentUser($this->company_id)
: $this->company_id;
$this->parseLastAuditDate();
$this->merge([
'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(),
'company_id' => $idForCurrentUser,
@ -48,4 +52,21 @@ class StoreAssetRequest extends ImageUploadRequest
return $rules;
}
private function parseLastAuditDate(): void
{
if ($this->input('last_audit_date')) {
try {
$lastAuditDate = Carbon::parse($this->input('last_audit_date'));
$this->merge([
'last_audit_date' => $lastAuditDate->startOfDay()->format('Y-m-d H:i:s'),
]);
} catch (InvalidFormatException $e) {
// we don't need to do anything here...
// we'll keep the provided date in an
// invalid format so validation picks it up later
}
}
}
}

View file

@ -90,27 +90,29 @@ class Asset extends Depreciable
];
protected $rules = [
'model_id' => 'required|integer|exists:models,id,deleted_at,NULL|not_array',
'status_id' => 'required|integer|exists:status_labels,id',
'asset_tag' => 'required|min:1|max:255|unique_undeleted:assets,asset_tag|not_array',
'name' => 'nullable|max:255',
'company_id' => 'nullable|integer|exists:companies,id',
'warranty_months' => 'nullable|numeric|digits_between:0,240',
'last_checkout' => 'nullable|date_format:Y-m-d H:i:s',
'model_id' => 'required|integer|exists:models,id,deleted_at,NULL|not_array',
'status_id' => 'required|integer|exists:status_labels,id',
'asset_tag' => 'required|min:1|max:255|unique_undeleted:assets,asset_tag|not_array',
'name' => 'nullable|max:255',
'company_id' => 'nullable|integer|exists:companies,id',
'warranty_months' => 'nullable|numeric|digits_between:0,240',
'last_checkout' => 'nullable|date_format:Y-m-d H:i:s',
'expected_checkin' => 'nullable|date',
'location_id' => 'nullable|exists:locations,id',
'rtd_location_id' => 'nullable|exists:locations,id',
'purchase_date' => 'nullable|date|date_format:Y-m-d',
'serial' => 'nullable|unique_undeleted:assets,serial',
'purchase_cost' => 'nullable|numeric|gte:0',
'supplier_id' => 'nullable|exists:suppliers,id',
'asset_eol_date' => 'nullable|date',
'eol_explicit' => 'nullable|boolean',
'byod' => 'nullable|boolean',
'order_number' => 'nullable|string|max:191',
'notes' => 'nullable|string|max:65535',
'assigned_to' => 'nullable|integer',
'requestable' => 'nullable|boolean',
'last_audit_date' => 'nullable|date_format:Y-m-d H:i:s',
'next_audit_date' => 'nullable|date|after:last_audit_date',
'location_id' => 'nullable|exists:locations,id',
'rtd_location_id' => 'nullable|exists:locations,id',
'purchase_date' => 'nullable|date|date_format:Y-m-d',
'serial' => 'nullable|unique_undeleted:assets,serial',
'purchase_cost' => 'nullable|numeric|gte:0',
'supplier_id' => 'nullable|exists:suppliers,id',
'asset_eol_date' => 'nullable|date',
'eol_explicit' => 'nullable|boolean',
'byod' => 'nullable|boolean',
'order_number' => 'nullable|string|max:191',
'notes' => 'nullable|string|max:65535',
'assigned_to' => 'nullable|integer',
'requestable' => 'nullable|boolean',
];
/**

View file

@ -5,6 +5,8 @@ namespace App\Models;
use Gate;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Watson\Validating\ValidatingTrait;
class CustomFieldset extends Model
@ -92,8 +94,19 @@ class CustomFieldset extends Model
array_push($rule, $field->attributes['format']);
$rules[$field->db_column_name()] = $rule;
//add not_array to rules for all fields
$rules[$field->db_column_name()][] = 'not_array';
// add not_array to rules for all fields but checkboxes
if ($field->element != 'checkbox') {
$rules[$field->db_column_name()][] = 'not_array';
}
if ($field->element == 'checkbox') {
$rules[$field->db_column_name()][] = 'checkboxes';
}
if ($field->element == 'radio') {
$rules[$field->db_column_name()][] = 'radio_buttons';
}
}
return $rules;

View file

@ -0,0 +1,89 @@
<?php
namespace App\Models\Labels\Tapes\Dymo;
class LabelWriter_1933081 extends LabelWriter
{
private const BARCODE_MARGIN = 1.80;
private const TAG_SIZE = 2.80;
private const TITLE_SIZE = 2.80;
private const TITLE_MARGIN = 0.50;
private const LABEL_SIZE = 2.80;
private const LABEL_MARGIN = - 0.35;
private const FIELD_SIZE = 2.80;
private const FIELD_MARGIN = 0.15;
public function getUnit() { return 'mm'; }
public function getWidth() { return 89; }
public function getHeight() { return 25; }
public function getSupportAssetTag() { return true; }
public function getSupport1DBarcode() { return true; }
public function getSupport2DBarcode() { return true; }
public function getSupportFields() { return 5; }
public function getSupportLogo() { return false; }
public function getSupportTitle() { return true; }
public function preparePDF($pdf) {}
public function write($pdf, $record) {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
$currentY = $pa->y1;
$usableWidth = $pa->w;
$barcodeSize = $pa->h - self::TAG_SIZE;
if ($record->has('barcode2d')) {
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $pa->y2 - self::TAG_SIZE,
'freesans', 'b', self::TAG_SIZE, 'C',
$barcodeSize, self::TAG_SIZE, true, 0
);
static::write2DBarcode(
$pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
$currentX, $currentY,
$barcodeSize, $barcodeSize
);
$currentX += $barcodeSize + self::BARCODE_MARGIN;
$usableWidth -= $barcodeSize + self::BARCODE_MARGIN;
} else {
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $pa->y2 - self::TAG_SIZE,
'freesans', 'b', self::TAG_SIZE, 'R',
$usableWidth, self::TAG_SIZE, true, 0
);
}
if ($record->has('title')) {
static::writeText(
$pdf, $record->get('title'),
$currentX, $currentY,
'freesans', 'b', self::TITLE_SIZE, 'L',
$usableWidth, self::TITLE_SIZE, true, 0
);
$currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
}
foreach ($record->get('fields') as $field) {
static::writeText(
$pdf, (($field['label']) ? $field['label'].' ' : '') . $field['value'],
$currentX, $currentY,
'freesans', '', self::FIELD_SIZE, 'L',
$usableWidth, self::FIELD_SIZE, true, 0, 0.3
);
$currentY += self::FIELD_SIZE + self::FIELD_MARGIN;
}
if ($record->has('barcode1d')) {
static::write1DBarcode(
$pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
$currentX, $barcodeSize + self::BARCODE_MARGIN, $usableWidth - self::TAG_SIZE, self::TAG_SIZE
);
}
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace App\Models\Labels\Tapes\Dymo;
class LabelWriter_2112283 extends LabelWriter
{
private const BARCODE_MARGIN = 1.80;
private const TAG_SIZE = 2.80;
private const TITLE_SIZE = 2.80;
private const TITLE_MARGIN = 0.50;
private const LABEL_SIZE = 2.80;
private const LABEL_MARGIN = - 0.35;
private const FIELD_SIZE = 2.80;
private const FIELD_MARGIN = 0.15;
public function getUnit() { return 'mm'; }
public function getWidth() { return 54; }
public function getHeight() { return 25; }
public function getSupportAssetTag() { return true; }
public function getSupport1DBarcode() { return true; }
public function getSupport2DBarcode() { return true; }
public function getSupportFields() { return 5; }
public function getSupportLogo() { return false; }
public function getSupportTitle() { return true; }
public function preparePDF($pdf) {}
public function write($pdf, $record) {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
$currentY = $pa->y1;
$usableWidth = $pa->w;
$barcodeSize = $pa->h - self::TAG_SIZE;
if ($record->has('barcode2d')) {
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $pa->y2 - self::TAG_SIZE,
'freesans', 'b', self::TAG_SIZE, 'C',
$barcodeSize, self::TAG_SIZE, true, 0
);
static::write2DBarcode(
$pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
$currentX, $currentY,
$barcodeSize, $barcodeSize
);
$currentX += $barcodeSize + self::BARCODE_MARGIN;
$usableWidth -= $barcodeSize + self::BARCODE_MARGIN;
} else {
static::writeText(
$pdf, $record->get('tag'),
$pa->x1, $pa->y2 - self::TAG_SIZE,
'freesans', 'b', self::TAG_SIZE, 'R',
$usableWidth, self::TAG_SIZE, true, 0
);
}
if ($record->has('title')) {
static::writeText(
$pdf, $record->get('title'),
$currentX, $currentY,
'freesans', 'b', self::TITLE_SIZE, 'L',
$usableWidth, self::TITLE_SIZE, true, 0
);
$currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
}
foreach ($record->get('fields') as $field) {
static::writeText(
$pdf, (($field['label']) ? $field['label'].' ' : '') . $field['value'],
$currentX, $currentY,
'freesans', '', self::FIELD_SIZE, 'L',
$usableWidth, self::FIELD_SIZE, true, 0, 0.3
);
$currentY += self::FIELD_SIZE + self::FIELD_MARGIN;
}
if ($record->has('barcode1d')) {
static::write1DBarcode(
$pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
$currentX, $barcodeSize + self::BARCODE_MARGIN, $usableWidth - self::TAG_SIZE, self::TAG_SIZE
);
}
}
}

View file

@ -41,6 +41,7 @@ class AccessoryPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'title' => trans('general.name'),
'formatter' => 'accessoriesLinkFormatter',
], [

View file

@ -38,10 +38,14 @@ class ActionlogPresenter extends Presenter
public function icon()
{
// User related icons
if ($this->itemType() == 'user') {
if ($this->actionType()=='2fa reset') {
return 'fa-solid fa-mobile-screen';
}
if ($this->actionType()=='create new') {
return 'fa-solid fa-user-plus';
}
@ -61,6 +65,7 @@ class ActionlogPresenter extends Presenter
if ($this->actionType()=='update') {
return 'fa-solid fa-user-pen';
}
return 'fa-solid fa-user';
}

View file

@ -85,6 +85,7 @@ class AssetMaintenancesPresenter extends Presenter
'field' => 'title',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'title' => trans('admin/asset_maintenances/form.title'),
], [
'field' => 'start_date',

View file

@ -35,6 +35,7 @@ class AssetModelPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'visible' => true,
'title' => trans('general.name'),
'formatter' => 'modelsLinkFormatter',

View file

@ -55,6 +55,7 @@ class AssetPresenter extends Presenter
'field' => 'asset_tag',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'title' => trans('admin/hardware/table.asset_tag'),
'visible' => true,
'formatter' => 'hardwareLinkFormatter',
@ -316,7 +317,7 @@ class AssetPresenter extends Presenter
'field' => 'checkincheckout',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'switchable' => false,
'title' => trans('general.checkin').'/'.trans('general.checkout'),
'visible' => true,
'formatter' => 'hardwareInOutFormatter',

View file

@ -25,6 +25,7 @@ class CategoryPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'title' => trans('general.name'),
'visible' => true,
'formatter' => 'categoriesLinkFormatter',

View file

@ -25,7 +25,7 @@ class CompanyPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'switchable' => true,
'switchable' => false,
'title' => trans('admin/companies/table.name'),
'visible' => true,
'formatter' => 'companiesLinkFormatter',

View file

@ -126,7 +126,7 @@ class ComponentPresenter extends Presenter
'field' => 'checkincheckout',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'switchable' => false,
'title' => trans('general.checkin').'/'.trans('general.checkout'),
'visible' => true,
'formatter' => 'componentsInOutFormatter',

View file

@ -35,6 +35,7 @@ class ConsumablePresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'title' => trans('general.name'),
'visible' => true,
'formatter' => 'consumablesLinkFormatter',

View file

@ -25,6 +25,7 @@ class DepreciationPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'title' => trans('general.name'),
'visible' => true,
'formatter' => 'depreciationsLinkFormatter',

View file

@ -34,6 +34,7 @@ class DepreciationReportPresenter extends Presenter
"field" => "name",
"searchable" => true,
"sortable" => true,
'switchable' => false,
"title" => trans('admin/hardware/form.name'),
"visible" => false,
], [

View file

@ -33,6 +33,7 @@ class LicensePresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'title' => trans('general.name'),
'formatter' => 'licensesLinkFormatter',
], [
@ -186,7 +187,7 @@ class LicensePresenter extends Presenter
'field' => 'checkincheckout',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'switchable' => false,
'title' => trans('general.checkin').'/'.trans('general.checkout'),
'visible' => true,
'formatter' => 'licensesInOutFormatter',
@ -280,7 +281,7 @@ class LicensePresenter extends Presenter
'field' => 'checkincheckout',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'switchable' => false,
'title' => trans('general.checkin').'/'.trans('general.checkout'),
'visible' => true,
'formatter' => 'licenseSeatInOutFormatter',

View file

@ -31,6 +31,7 @@ class LocationPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'title' => trans('admin/locations/table.name'),
'visible' => true,
'formatter' => 'locationsLinkFormatter',

View file

@ -27,6 +27,7 @@ class ManufacturerPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'switchable' => false,
'title' => trans('admin/manufacturers/table.name'),
'visible' => true,
'formatter' => 'manufacturersLinkFormatter',

View file

@ -38,7 +38,7 @@ class UserPresenter extends Presenter
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => 'Avatar',
'title' => trans('general.importer.avatar'),
'visible' => false,
'formatter' => 'imageFormatter',
],
@ -175,7 +175,7 @@ class UserPresenter extends Presenter
'field' => 'username',
'searchable' => true,
'sortable' => true,
'switchable' => true,
'switchable' => false,
'title' => trans('admin/users/table.username'),
'visible' => true,
'formatter' => 'usersLinkFormatter',

View file

@ -2,9 +2,12 @@
namespace App\Providers;
use App\Models\CustomField;
use App\Models\Department;
use App\Models\Setting;
use DB;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rule;
use Validator;
@ -293,6 +296,39 @@ class ValidationServiceProvider extends ServiceProvider
Validator::extend('not_array', function ($attribute, $value, $parameters, $validator) {
return !is_array($value);
});
// This is only used in Models/CustomFieldset.php - it does automatic validation for checkboxes by making sure
// that the submitted values actually exist in the options.
Validator::extend('checkboxes', function ($attribute, $value, $parameters, $validator){
$field = CustomField::where('db_column', $attribute)->first();
$options = $field->formatFieldValuesAsArray();
if(is_array($value)) {
$invalid = array_diff($value, $options);
if(count($invalid) > 0) {
return false;
}
}
// for legacy, allows users to submit a comma separated string of options
elseif(!is_array($value)) {
$exploded = array_map('trim', explode(',', $value));
$invalid = array_diff($exploded, $options);
if(count($invalid) > 0) {
return false;
}
}
return true;
});
// Validates that a radio button option exists
Validator::extend('radio_buttons', function ($attribute, $value) {
$field = CustomField::where('db_column', $attribute)->first();
$options = $field->formatFieldValuesAsArray();
return in_array($value, $options);
});
}
/**

View file

@ -174,4 +174,17 @@ return [
'bs_table_storage' => env('BS_TABLE_STORAGE', 'cookieStorage'),
/*
|--------------------------------------------------------------------------
| Bootstrap Table Enable Deeplinking
|--------------------------------------------------------------------------
|
| Use deeplinks to directly link to search results, sorting, and pagination
|
| More info: https://github.com/generals-space/bootstrap-table-addrbar/blob/master/readme(EN).md
*/
'bs_table_addrbar' => env('BS_TABLE_DEEPLINK', true),
];

View file

@ -1,10 +1,10 @@
<?php
return array (
'app_version' => 'v6.3.3',
'full_app_version' => 'v6.3.3 - build 12903-g0f63fa23e',
'build_version' => '12903',
'full_app_version' => 'v6.3.3 - build 13056-gb34156ca2',
'build_version' => '13056',
'prerelease_version' => '',
'hash_version' => 'g0f63fa23e',
'full_hash' => 'v6.3.3-67-g0f63fa23e',
'hash_version' => 'gb34156ca2',
'full_hash' => 'v6.3.3-151-gb34156ca2',
'branch' => 'develop',
);

6
package-lock.json generated
View file

@ -2379,9 +2379,9 @@
}
},
"alpinejs": {
"version": "3.13.5",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.5.tgz",
"integrity": "sha512-1d2XeNGN+Zn7j4mUAKXtAgdc4/rLeadyTMWeJGXF5DzwawPBxwTiBhFFm6w/Ei8eJxUZeyNWWSD9zknfdz1kEw==",
"version": "3.13.7",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.7.tgz",
"integrity": "sha512-rcTyjTANbsePq1hb7eSekt3qjI94HLGeO6JaRjCssCVbIIc+qBrc7pO5S/+2JB6oojIibjM6FA+xRI3zhGPZIg==",
"requires": {
"@vue/reactivity": "~3.1.1"
}

View file

@ -33,7 +33,7 @@
"acorn-import-assertions": "^1.9.0",
"admin-lte": "^2.4.18",
"ajv": "^6.12.6",
"alpinejs": "^3.13.5",
"alpinejs": "^3.13.6",
"blueimp-file-upload": "^9.34.0",
"bootstrap": "^3.4.1",
"bootstrap-colorpicker": "^2.5.3",

Binary file not shown.

Binary file not shown.

View file

@ -31,9 +31,9 @@
"/css/webfonts/fa-v4compatibility.woff2": "/css/webfonts/fa-v4compatibility.woff2?id=e11465c0eff0549edd4e8ea6bbcf242f",
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=99c395f0bab5966f32f63f4e55899e64",
"/js/build/vendor.js": "/js/build/vendor.js?id=a2b971da417306a63385c8098acfe4af",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=d0eb38da8b772a21b827b7df208dc4fe",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=857da5daffd13e0553510e5ccd410c79",
"/js/dist/all.js": "/js/dist/all.js?id=13bdb521e0c745d7f81dae3fb110b650",
"/js/dist/all-defer.js": "/js/dist/all-defer.js?id=19ccc62a8f1ea103dede4808837384d4",
"/js/dist/all-defer.js": "/js/dist/all-defer.js?id=18d36546bdad8285c229008df799b343",
"/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=0a82a6ae6bb4e58fe62d162c4fb50397",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=d419cb63a12dc175d71645c876bfc2ab",
"/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=76482123f6c70e866d6b971ba91de7bb",

View file

@ -261,7 +261,7 @@ return [
'two_factor_enrollment' => 'Two-Factor Enrollment',
'two_factor_enabled_text' => 'Enable Two Factor',
'two_factor_reset' => 'Reset Two-Factor Secret',
'two_factor_reset_help' => 'This will force the user to enroll their device with Google Authenticator again. This can be useful if their currently enrolled device is lost or stolen. ',
'two_factor_reset_help' => 'This will force the user to enroll their device with their authenticator app again. This can be useful if their currently enrolled device is lost or stolen. ',
'two_factor_reset_success' => 'Two factor device successfully reset',
'two_factor_reset_error' => 'Two factor device reset failed',
'two_factor_enabled_warning' => 'Enabling two-factor if it is not currently enabled will immediately force you to authenticate with a Google Auth enrolled device. You will have the ability to enroll your device if one is not currently enrolled.',

View file

@ -1,6 +1,7 @@
<?php
return [
'2FA_reset' => '2FA reset',
'accessories' => 'Accessories',
'activated' => 'Activated',
'accepted_date' => 'Date Accepted',

View file

@ -105,6 +105,8 @@ return [
'gte' => [
'numeric' => 'Value cannot be negative'
],
'checkboxes' => ':attribute contains invalid options.',
'radio_buttons' => ':attribute is invalid.',
/*
@ -151,4 +153,10 @@ return [
'attributes' => [],
/*
|--------------------------------------------------------------------------
| Generic Validation Messages
|--------------------------------------------------------------------------
*/
'invalid_value_in_field' => 'Invalid value included in this field',
];

View file

@ -135,7 +135,7 @@
@if (!$field->id)
<!-- Encrypted -->
<div class="col-md-9 col-md-offset-3">
<div class="col-md-9 col-md-offset-3" id="encryption_section">
<label class="form-control">
<input type="checkbox" value="1" name="field_encrypted" id="field_encrypted"{{ (Request::old('field_encrypted') || $field->field_encrypted) ? ' checked="checked"' : '' }}>
{{ trans('admin/custom_fields/general.encrypt_field') }}
@ -146,7 +146,6 @@
<p><i class="fas fa-exclamation-triangle" aria-hidden="true"></i> {{ trans('admin/custom_fields/general.encrypt_field_help') }}</p>
</div>
</div>
@endif
@ -298,11 +297,16 @@
}).change();
// Only display the field element if the type is not text
// and don't display encryption option for checkbox or radio
$(".field_element").change(function(){
$(this).find("option:selected").each(function(){
if (($(this).attr("value")!="text") && ($(this).attr("value")!="textarea")){
$("#field_values_text").show();
if ($(this).attr("value") == "checkbox" || $(this).attr("value") == "radio") {
$("#encryption_section").hide();
}
} else{
$("#encryption_section").show();
$("#field_values_text").hide();
}
});

View file

@ -904,27 +904,18 @@
@endcan
@can('delete', $asset)
@if ($asset->deleted_at=='')
<div class="col-md-12" style="padding-top: 30px; padding-bottom: 30px;">
<button class="btn btn-block btn-danger delete-asset" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.sure_to_delete', ['item' => $asset->asset_tag]) }}" data-target="#dataConfirmModal">{{ trans('general.delete') }} </button>
<div class="col-md-12" style="padding-top: 30px; padding-bottom: 30px;">
@if ($asset->deleted_at=='')
<button class="btn btn-sm btn-block btn-danger delete-asset" data-toggle="modal" data-title="{{ trans('general.delete') }}" data-content="{{ trans('general.sure_to_delete_var', ['item' => $asset->asset_tag]) }}" data-target="#dataConfirmModal">{{ trans('general.delete') }} </button>
<span class="sr-only">{{ trans('general.delete') }}</span>
</div>
@endif
@else
<form method="POST" action="{{ route('restore/hardware', ['assetId' => $asset->id]) }}">
@csrf
<button class="btn btn-sm btn-warning col-md-12">{{ trans('general.restore') }}</button>
</form>
@endif
@endcan
@if ($asset->deleted_at!='')
<div class="text-center col-md-12" style="padding-top: 30px; padding-bottom: 30px;">
<form method="POST" action="{{ route('restore/hardware', ['assetId' => $asset->id]) }}">
@csrf
<button class="btn btn-danger col-md-12">{{ trans('general.restore') }}</button>
</form>
</div>
@endif
@if ($snipeSettings->qr_code=='1')
<img src="{{ config('app.url') }}/hardware/{{ $asset->id }}/qr_code" class="img-thumbnail pull-right" style="height: 100px; width: 100px; margin-right: 10px;" alt="QR code for {{ $asset->getDisplayNameAttribute() }}">
@endif
@if (($asset->assignedTo) && ($asset->deleted_at==''))
<div style="text-align: left">
<h2>{{ trans('admin/hardware/form.checkedout_to') }}</h2>
@ -982,9 +973,17 @@
</div>
@endif
@if ($snipeSettings->qr_code=='1')
<div class="col-md-12" style="padding-top: 15px;">
<img src="{{ config('app.url') }}/hardware/{{ $asset->id }}/qr_code" class="img-thumbnail pull-right" style="height: 100px; width: 100px; margin-right: 10px;" alt="QR code for {{ $asset->getDisplayNameAttribute() }}">
</div>
@endif
</div> <!-- div.col-md-4 -->
</div><!-- /row -->
</div><!-- /.tab-pane asset details -->
</div>
<div class="tab-pane fade" id="software">
<div class="row">

View file

@ -174,23 +174,56 @@
@endif
@if ($license->supplier_id)
<div class="row">
<div class="col-md-3">
<strong>
{{ trans('general.supplier') }}
</strong>
</div>
<div class="col-md-9">
@if ($license->supplier)
<a href="{{ route('suppliers.show', $license->supplier_id) }}">
@if ($license->supplier)
<div class="row">
<div class="col-md-3">
<strong>{{ trans('general.supplier') }}</strong>
</div>
<div class="col-md-9">
@can('view', \App\Models\Supplier::class)
<a href="{{ route('suppliers.show', $license->supplier->id) }}">
{{ $license->supplier->name }}
</a>
@else
{{ $license->supplier->name }}
</a>
@else
{{ trans('general.deleted') }}
@endif
@endcan
@if ($license->supplier->url)
<br><i class="fas fa-globe-americas" aria-hidden="true"></i> <a href="{{ $license->supplier->url }}" rel="noopener">{{ $license->supplier->url }}</a>
@endif
@if ($license->supplier->phone)
<br><i class="fas fa-phone" aria-hidden="true"></i>
<a href="tel:{{ $license->supplier->phone }}">{{ $license->supplier->phone }}</a>
@endif
@if ($license->supplier->email)
<br><i class="far fa-envelope" aria-hidden="true"></i> <a href="mailto:{{ $license->supplier->email }}">{{ $license->supplier->email }}</a>
@endif
@if ($license->supplier->address)
<br>{{ $license->supplier->address }}
@endif
@if ($license->supplier->address2)
<br>{{ $license->supplier->address2 }}
@endif
@if ($license->supplier->city)
<br>{{ $license->supplier->city }},
@endif
@if ($license->supplier->state)
{{ $license->supplier->state }}
@endif
@if ($license->supplier->country)
{{ $license->supplier->country }}
@endif
@if ($license->supplier->zip)
{{ $license->supplier->zip }}
@endif
</div>
</div>
</div>
@else
{{ trans('general.deleted') }}
@endif

View file

@ -9,7 +9,7 @@
<!-- Listbox -->
@if ($field->element=='listbox')
{{ Form::select($field->db_column_name(), $field->formatFieldValuesAsArray(),
Request::old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, htmlspecialchars($item->{$field->db_column_name()}, ENT_QUOTES)) : $field->defaultValue($model->id))), ['class'=>'format select2 form-control']) }}
Request::old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))), ['class'=>'format select2 form-control']) }}
@elseif ($field->element=='textarea')
<textarea class="col-md-6 form-control" id="{{ $field->db_column_name() }}" name="{{ $field->db_column_name() }}">{{ Request::old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))) }}</textarea>

View file

@ -25,7 +25,7 @@
<!-- Listbox -->
@if ($field->element=='listbox')
{{ Form::select($field->db_column_name(), $field->formatFieldValuesAsArray(),
Request::old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, htmlspecialchars($item->{$field->db_column_name()}, ENT_QUOTES)) : $field->defaultValue($model->id))), ['class'=>'format select2 form-control']) }}
Request::old($field->db_column_name(),(isset($item) ? Helper::gracefulDecrypt($field, $item->{$field->db_column_name()}) : $field->defaultValue($model->id))), ['class'=>'format select2 form-control']) }}
@elseif ($field->element=='textarea')
@if($field->is_unique)

View file

@ -236,6 +236,12 @@
</li>
@endif
@if ($model->created_at)
<li>{{ trans('general.created_at') }}:
{{ Helper::getFormattedDateObject($model->created_at, 'datetime', false) }}
</li>
@endif
@if ($model->min_amt)
<li>{{ trans('general.min_amt') }}:
{{$model->min_amt }}
@ -313,11 +319,6 @@
</li>
@endif
@if ($model->deleted_at!='')
<li><br /><a href="{{ route('models.restore.store', $model->id) }}" class="btn-flat large info ">{{ trans('admin/models/general.restore') }}</a></li>
@endif
</ul>
@if ($model->note)
@ -337,22 +338,32 @@
@can('create', \App\Models\AssetModel::class)
<div class="col-md-12" style="padding-bottom: 5px;">
<a href="{{ route('models.clone.create', $model->id) }}" style="width: 100%;" class="btn btn-sm btn-warning hidden-print">{{ trans('admin/models/table.clone') }}</a>
<a href="{{ route('models.clone.create', $model->id) }}" style="width: 100%;" class="btn btn-sm btn-primary hidden-print">{{ trans('admin/models/table.clone') }}</a>
</div>
@endcan
@can('delete', \App\Models\AssetModel::class)
@if ($model->assets_count > 0)
<div class="col-md-12" style="padding-bottom: 5px;">
<button class="btn btn-block btn-sm btn-danger hidden-print disabled" data-tooltip="true" data-placement="top" data-title="{{ trans('general.cannot_be_deleted') }}">{{ trans('general.delete') }}</button>
<button class="btn btn-block btn-sm btn-primary hidden-print disabled" data-tooltip="true" data-placement="top" data-title="{{ trans('general.cannot_be_deleted') }}">{{ trans('general.delete') }}</button>
</div>
@else
<div class="col-md-12" style="padding-bottom: 10px;">
<button class="btn btn-block btn-danger delete-asset" data-toggle="modal" title="{{ trans('general.delete_what', ['item'=> trans('general.asset_model')]) }}" data-content="{{ trans('general.sure_to_delete_var', ['item' => $model->name]) }}" data-target="#dataConfirmModal" data-tooltip="true" data-placement="top" data-title="{{ trans('general.delete_what', ['item'=> trans('general.asset_model')]) }}">{{ trans('general.delete') }} </button>
<span class="sr-only">{{ trans('general.delete') }}</span>
</div>
@endif
<div class="text-center col-md-12" style="padding-top: 30px; padding-bottom: 30px;">
@if ($model->deleted_at!='')
<form method="POST" action="{{ route('models.restore.store', $model->id) }}">
@csrf
<button style="width: 100%;" class="btn btn-sm btn-warning hidden-print">{{ trans('button.restore') }}</button>
</form>
@else
<button class="btn btn-block btn-sm btn-danger delete-asset" data-toggle="modal" title="{{ trans('general.delete_what', ['item'=> trans('general.asset_model')]) }}" data-content="{{ trans('general.sure_to_delete_var', ['item' => $model->name]) }}" data-target="#dataConfirmModal" data-tooltip="true" data-placement="top" data-title="{{ trans('general.delete_what', ['item'=> trans('general.asset_model')]) }}">{{ trans('general.delete') }} </button>
<span class="sr-only">{{ trans('general.delete') }}</span>
@endif
</div>
@endcan
</div>
</div> <!-- /.row -->

View file

@ -46,16 +46,19 @@
stickyHeader: true,
stickyHeaderOffsetLeft: parseInt($('body').css('padding-left'), 10),
stickyHeaderOffsetRight: parseInt($('body').css('padding-right'), 10),
locale: locale,
locale: '{{ app()->getLocale() }}',
undefinedText: '',
iconsPrefix: 'fa',
cookieStorage: '{{ config('session.bs_table_storage') }}',
cookie: true,
cookieExpire: '2y',
showColumnsToggleAll: true,
minimumCountColumns: 2,
mobileResponsive: true,
maintainSelected: true,
trimOnSearch: false,
showSearchClearButton: true,
addrbar: {{ (config('session.bs_table_addrbar') == 'true') ? 'true' : 'false'}}, // deeplink search phrases, sorting, etc
paginationFirstText: "{{ trans('general.first') }}",
paginationLastText: "{{ trans('general.last') }}",
paginationPreText: "{{ trans('general.previous') }}",
@ -85,7 +88,7 @@
export: 'fa-download',
clearSearch: 'fa-times'
},
exportOptions: export_options,
exportOptions: export_options,
exportTypes: ['xlsx', 'excel', 'csv', 'pdf','json', 'xml', 'txt', 'sql', 'doc' ],
onLoadSuccess: function () {
$('[data-tooltip="true"]').tooltip(); // Needed to attach tooltips after ajax call

View file

@ -74,12 +74,11 @@
<!-- Common Passwords -->
<div class="form-group">
<div class="form-group {{ $errors->has('pwd_secure_complexity.*') ? 'error' : '' }}">
<div class="col-md-3">
{{ Form::label('pwd_secure_complexity', trans('admin/settings/general.pwd_secure_complexity')) }}
</div>
<div class="col-md-9">
<label class="form-control">
<span class="sr-only">{{ trans('admin/settings/general.pwd_secure_uncommon') }}</span>
{{ Form::checkbox('pwd_secure_uncommon', '1', old('pwd_secure_uncommon', $setting->pwd_secure_uncommon),array( 'aria-label'=>'pwd_secure_uncommon')) }}
@ -106,6 +105,9 @@
{{ trans('admin/settings/general.pwd_secure_complexity_case_diff') }}
</label>
@if ($errors->has('pwd_secure_complexity.*'))
<span class="alert-msg">{{ trans('validation.invalid_value_in_field') }}</span>
@endif
<p class="help-block">
{{ trans('admin/settings/general.pwd_secure_complexity_help') }}
</p>

View file

@ -499,18 +499,21 @@
</div>
@endif
<!-- Reset Two Factor -->
<div class="form-group">
<div class="col-md-8 col-md-offset-3 two_factor_resetrow">
<a class="btn btn-default btn-sm pull-left" id="two_factor_reset" style="margin-right: 10px;"> {{ trans('admin/settings/general.two_factor_reset') }}</a>
<span id="two_factor_reseticon"></span>
<span id="two_factor_resetresult"></span>
<span id="two_factor_resetstatus"></span>
@if ((Auth::user()->isSuperUser()) && ($user->two_factor_active_and_enrolled()) && ($snipeSettings->two_factor_enabled!='0') && ($snipeSettings->two_factor_enabled!=''))
<!-- Reset Two Factor -->
<div class="form-group">
<div class="col-md-8 col-md-offset-3 two_factor_resetrow">
<a class="btn btn-default btn-sm pull-left" id="two_factor_reset" style="margin-right: 10px;"> {{ trans('admin/settings/general.two_factor_reset') }}</a>
<span id="two_factor_reseticon"></span>
<span id="two_factor_resetresult"></span>
<span id="two_factor_resetstatus"></span>
</div>
<div class="col-md-8 col-md-offset-3 two_factor_resetrow">
<p class="help-block">{{ trans('admin/settings/general.two_factor_reset_help') }}</p>
</div>
</div>
<div class="col-md-8 col-md-offset-3 two_factor_resetrow">
<p class="help-block">{{ trans('admin/settings/general.two_factor_reset_help') }}</p>
</div>
</div>
@endif
@endif
<!-- Groups -->
@ -702,7 +705,7 @@ $(document).ready(function() {
$("#two_factor_resetrow").removeClass('success');
$("#two_factor_resetrow").removeClass('danger');
$("#two_factor_resetstatus").html('');
$("#two_factor_reseticon").html('<i class="fas fa-spinner spin"></i>');
$("#two_factor_reseticon").html('<i class="fas fa-spinner spin"></i> ');
$.ajax({
url: '{{ route('api.users.two_factor_reset', ['id'=> $user->id]) }}',
type: 'POST',
@ -715,13 +718,12 @@ $(document).ready(function() {
success: function (data) {
$("#two_factor_reseticon").html('');
$("#two_factor_resetstatus").html('<i class="fas fa-check text-success"></i>' + data.message);
$("#two_factor_resetstatus").html('<span class="text-success"><i class="fas fa-check"></i> ' + data.message + '</span>');
},
error: function (data) {
$("#two_factor_reseticon").html('');
$("#two_factor_reseticon").html('<i class="fas fa-exclamation-triangle text-danger"></i>');
$('#two_factor_resetstatus').text(data.message);
$("#two_factor_resetstatus").html('<span class="text-danger"><i class="fas fa-exclamation-triangle text-danger"></i> ' + data.message + '</span>');
}

View file

@ -110,6 +110,7 @@
<th data-field="asset_name" data-sortable="true" data-visible="true">{{ trans('general.name') }}</th>
<th data-field="asset_category" data-sortable="true" data-visible="true">{{ trans('general.category') }}</th>
<th data-field="asset_model" data-sortable="true" data-visible="true">{{ trans('admin/hardware/form.model') }}</th>
<th data-field="rtd_location" data-sortable="true" data-visible="true">{{ trans('admin/hardware/form.default_location') }}</th>
<th data-field="asset_location" data-sortable="true" data-visible="false">{{ trans('general.location') }}</th>
<th data-field="asset_serial" data-sortable="true" data-visible="true">{{ trans('admin/hardware/form.serial') }}</th>
<th data-field="asset_checkout_date" data-sortable="true" data-visible="true">{{ trans('admin/hardware/table.checkout_date') }}</th>
@ -129,6 +130,7 @@
<td>{{ $asset->name }}</td>
<td>{{ (($asset->model) && ($asset->model->category)) ? $asset->model->category->name : trans('general.invalid_category') }}</td>
<td>{{ ($asset->model) ? $asset->model->name : trans('general.invalid_model') }}</td>
<td>{{ ($asset->defaultLoc) ? $asset->defaultLoc->name : '' }}</td>
<td>{{ ($asset->location) ? $asset->location->name : '' }}</td>
<td>{{ $asset->serial }}</td>
<td>
@ -155,6 +157,7 @@
<td>{{ $asset->asset_tag }}</td>
<td>{{ $asset->name }}</td>
<td>{{ $asset->model->category->name }}</td>
<td>{{ ($asset->defaultLoc) ? $asset->defaultLoc->name : '' }}</td>
<td>{{ ($asset->location) ? $asset->location->name : '' }}</td>
<td>{{ $asset->model->name }}</td>
<td>{{ $asset->serial }}</td>

View file

@ -597,7 +597,7 @@
</div>
</div>
@if ((Auth::user()->isSuperUser()) && ($snipeSettings->two_factor_enabled!='0') && ($snipeSettings->two_factor_enabled!=''))
@if ((Auth::user()->isSuperUser()) && ($user->two_factor_active_and_enrolled()) && ($snipeSettings->two_factor_enabled!='0') && ($snipeSettings->two_factor_enabled!=''))
<!-- 2FA reset -->
<div class="row">

View file

@ -7,13 +7,10 @@ use App\Models\Actionlog;
use App\Models\User;
use App\Notifications\CheckoutAccessoryNotification;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AccessoryCheckoutTest extends TestCase
{
use InteractsWithSettings;
public function testCheckingOutAccessoryRequiresCorrectPermission()
{
$this->actingAsForApi(User::factory()->create())

View file

@ -11,13 +11,10 @@ use App\Models\Statuslabel;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Event;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetCheckinTest extends TestCase
{
use InteractsWithSettings;
public function testCheckingInAssetRequiresCorrectPermission()
{
$this->actingAsForApi(User::factory()->create())

View file

@ -6,13 +6,10 @@ use App\Models\Asset;
use App\Models\Company;
use App\Models\User;
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetIndexTest extends TestCase
{
use InteractsWithSettings;
public function testAssetIndexReturnsExpectedAssets()
{
Asset::factory()->count(3)->create();

View file

@ -9,15 +9,11 @@ use App\Models\Location;
use App\Models\Statuslabel;
use App\Models\Supplier;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetStoreTest extends TestCase
{
use InteractsWithSettings;
public function testRequiresPermissionToCreateAsset()
{
$this->actingAsForApi(User::factory()->create())
@ -69,8 +65,7 @@ class AssetStoreTest extends TestCase
$this->assertEquals('random_string', $asset->asset_tag);
$this->assertEquals($userAssigned->id, $asset->assigned_to);
$this->assertTrue($asset->company->is($company));
// I don't see this on the GUI side either, but it's in the docs so I'm guessing that's a mistake? It wasn't in the controller.
// $this->assertEquals('2023-09-03', $asset->last_audit_date);
$this->assertEquals('2023-09-03 00:00:00', $asset->last_audit_date->format('Y-m-d H:i:s'));
$this->assertTrue($asset->location->is($location));
$this->assertTrue($asset->model->is($model));
$this->assertEquals('A New Asset', $asset->name);
@ -86,6 +81,52 @@ class AssetStoreTest extends TestCase
$this->assertEquals(10, $asset->warranty_months);
}
public function testSetsLastAuditDateToMidnightOfProvidedDate()
{
$response = $this->actingAsForApi(User::factory()->superuser()->create())
->postJson(route('api.assets.store'), [
'last_audit_date' => '2023-09-03 12:23:45',
'asset_tag' => '1234',
'model_id' => AssetModel::factory()->create()->id,
'status_id' => Statuslabel::factory()->create()->id,
])
->assertOk()
->assertStatusMessageIs('success');
$asset = Asset::find($response['payload']['id']);
$this->assertEquals('00:00:00', $asset->last_audit_date->format('H:i:s'));
}
public function testLastAuditDateCanBeNull()
{
$response = $this->actingAsForApi(User::factory()->superuser()->create())
->postJson(route('api.assets.store'), [
// 'last_audit_date' => '2023-09-03 12:23:45',
'asset_tag' => '1234',
'model_id' => AssetModel::factory()->create()->id,
'status_id' => Statuslabel::factory()->create()->id,
])
->assertOk()
->assertStatusMessageIs('success');
$asset = Asset::find($response['payload']['id']);
$this->assertNull($asset->last_audit_date);
}
public function testNonDateUsedForLastAuditDateReturnsValidationError()
{
$response = $this->actingAsForApi(User::factory()->superuser()->create())
->postJson(route('api.assets.store'), [
'last_audit_date' => 'this-is-not-valid',
'asset_tag' => '1234',
'model_id' => AssetModel::factory()->create()->id,
'status_id' => Statuslabel::factory()->create()->id,
])
->assertStatusMessageIs('error');
$this->assertNotNull($response->json('messages.last_audit_date'));
}
public function testArchivedDepreciateAndPhysicalCanBeNull()
{
$model = AssetModel::factory()->ipadModel()->create();

View file

@ -5,13 +5,10 @@ namespace Tests\Feature\Api\Assets;
use App\Models\Asset;
use App\Models\Company;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetsForSelectListTest extends TestCase
{
use InteractsWithSettings;
public function testAssetsCanBeSearchedForByAssetTag()
{
Asset::factory()->create(['asset_tag' => '0001']);

View file

@ -5,13 +5,10 @@ namespace Tests\Feature\Api\Assets;
use App\Models\Asset;
use App\Models\Company;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class RequestableAssetsTest extends TestCase
{
use InteractsWithSettings;
public function testViewingRequestableAssetsRequiresCorrectPermission()
{
$this->actingAsForApi(User::factory()->create())

View file

@ -5,13 +5,10 @@ namespace Tests\Feature\Api\Components;
use App\Models\Company;
use App\Models\Component;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class ComponentIndexTest extends TestCase
{
use InteractsWithSettings;
public function testComponentIndexAdheresToCompanyScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();

View file

@ -7,13 +7,10 @@ use App\Models\Consumable;
use App\Models\User;
use App\Notifications\CheckoutConsumableNotification;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class ConsumableCheckoutTest extends TestCase
{
use InteractsWithSettings;
public function testCheckingOutConsumableRequiresCorrectPermission()
{
$this->actingAsForApi(User::factory()->create())

View file

@ -5,13 +5,10 @@ namespace Tests\Feature\Api\Consumables;
use App\Models\Company;
use App\Models\Consumable;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class ConsumablesIndexTest extends TestCase
{
use InteractsWithSettings;
public function testConsumableIndexAdheresToCompanyScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();

View file

@ -5,15 +5,11 @@ namespace Tests\Feature\Api\Departments;
use App\Models\Company;
use App\Models\Department;
use App\Models\User;
use Illuminate\Routing\Route;
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class DepartmentIndexTest extends TestCase
{
use InteractsWithSettings;
public function testViewingDepartmentIndexRequiresAuthentication()
{
$this->getJson(route('api.departments.index'))->assertRedirect();

View file

@ -4,13 +4,10 @@ namespace Tests\Feature\Api\Groups;
use App\Models\Group;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class GroupStoreTest extends TestCase
{
use InteractsWithSettings;
public function testStoringGroupRequiresSuperAdminPermission()
{
$this->actingAsForApi(User::factory()->create())

View file

@ -5,13 +5,10 @@ namespace Tests\Feature\Api\Licenses;
use App\Models\Company;
use App\Models\License;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class LicensesIndexTest extends TestCase
{
use InteractsWithSettings;
public function testLicensesIndexAdheresToCompanyScoping()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();

View file

@ -5,13 +5,10 @@ namespace Tests\Feature\Api\Locations;
use App\Models\Location;
use App\Models\User;
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class LocationsForSelectListTest extends TestCase
{
use InteractsWithSettings;
public function testGettingLocationListRequiresProperPermission()
{
$this->actingAsForApi(User::factory()->create())

View file

@ -3,13 +3,10 @@
namespace Tests\Feature\Api\Users;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class UpdateUserApiTest extends TestCase
{
use InteractsWithSettings;
public function testApiUsersCanBeActivatedWithNumber()
{
$admin = User::factory()->superuser()->create();

View file

@ -6,13 +6,10 @@ use App\Models\Company;
use App\Models\User;
use Illuminate\Testing\Fluent\AssertableJson;
use Laravel\Passport\Passport;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class UsersForSelectListTest extends TestCase
{
use InteractsWithSettings;
public function testUsersAreReturned()
{
$users = User::factory()->superuser()->count(3)->create();

View file

@ -5,13 +5,10 @@ namespace Tests\Feature\Api\Users;
use App\Models\Company;
use App\Models\User;
use Laravel\Passport\Passport;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class UsersSearchTest extends TestCase
{
use InteractsWithSettings;
public function testCanSearchByUserFirstAndLastName()
{
User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Skywalker']);

View file

@ -8,13 +8,10 @@ use App\Models\Group;
use App\Models\Location;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class UsersUpdateTest extends TestCase
{
use InteractsWithSettings;
public function testCanUpdateUserViaPatch()
{
$admin = User::factory()->superuser()->create();

View file

@ -8,13 +8,10 @@ use App\Models\User;
use App\Notifications\CheckinAccessoryNotification;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AccessoryCheckinTest extends TestCase
{
use InteractsWithSettings;
public function testCheckingInAccessoryRequiresCorrectPermission()
{
$accessory = Accessory::factory()->checkedOutToUser()->create();

View file

@ -11,13 +11,10 @@ use App\Models\Statuslabel;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Event;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetCheckinTest extends TestCase
{
use InteractsWithSettings;
public function testCheckingInAssetRequiresCorrectPermission()
{
$this->actingAs(User::factory()->create())

View file

@ -7,13 +7,10 @@ use App\Models\CheckoutAcceptance;
use App\Notifications\AcceptanceAssetAcceptedNotification;
use App\Notifications\AcceptanceAssetDeclinedNotification;
use Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AccessoryAcceptanceTest extends TestCase
{
use InteractsWithSettings;
/**
* This can be absorbed into a bigger test
*/

View file

@ -7,13 +7,10 @@ use App\Models\Actionlog;
use App\Models\User;
use App\Notifications\CheckoutAccessoryNotification;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AccessoryCheckoutTest extends TestCase
{
use InteractsWithSettings;
public function testCheckingOutAccessoryRequiresCorrectPermission()
{
$this->actingAs(User::factory()->create())

View file

@ -7,13 +7,10 @@ use App\Models\Consumable;
use App\Models\User;
use App\Notifications\CheckoutConsumableNotification;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class ConsumableCheckoutTest extends TestCase
{
use InteractsWithSettings;
public function testCheckingOutConsumableRequiresCorrectPermission()
{
$this->actingAs(User::factory()->create())

View file

@ -6,13 +6,10 @@ use App\Models\Asset;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class LicenseCheckoutTest extends TestCase
{
use InteractsWithSettings;
public function testNotesAreStoredInActionLogOnCheckoutToAsset()
{
$admin = User::factory()->superuser()->create();

View file

@ -3,13 +3,10 @@
namespace Tests\Feature;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class DashboardTest extends TestCase
{
use InteractsWithSettings;
public function testUsersWithoutAdminAccessAreRedirected()
{
$this->actingAs(User::factory()->create())

View file

@ -7,7 +7,6 @@ use App\Models\Asset;
use App\Models\User;
use App\Notifications\CheckinAssetNotification;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
/**
@ -15,8 +14,6 @@ use Tests\TestCase;
*/
class EmailNotificationsUponCheckinTest extends TestCase
{
use InteractsWithSettings;
protected function setUp(): void
{
parent::setUp();

View file

@ -14,7 +14,6 @@ use App\Notifications\CheckinAssetNotification;
use App\Notifications\CheckinLicenseSeatNotification;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
/**
@ -22,8 +21,6 @@ use Tests\TestCase;
*/
class SlackNotificationsUponCheckinTest extends TestCase
{
use InteractsWithSettings;
protected function setUp(): void
{
parent::setUp();

View file

@ -16,7 +16,6 @@ use App\Notifications\CheckoutConsumableNotification;
use App\Notifications\CheckoutLicenseSeatNotification;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
/**
@ -24,8 +23,6 @@ use Tests\TestCase;
*/
class SlackNotificationsUponCheckoutTest extends TestCase
{
use InteractsWithSettings;
protected function setUp(): void
{
parent::setUp();

View file

@ -8,14 +8,10 @@ use App\Models\User;
use Illuminate\Testing\TestResponse;
use League\Csv\Reader;
use PHPUnit\Framework\Assert;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class CustomReportTest extends TestCase
{
use InteractsWithSettings;
protected function setUp(): void
{
parent::setUp();

View file

@ -3,13 +3,10 @@
namespace Tests\Feature\Users;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class UpdateUserTest extends TestCase
{
use InteractsWithSettings;
public function testUsersCanBeActivatedWithNumber()
{
$admin = User::factory()->superuser()->create();

View file

@ -4,7 +4,7 @@ namespace Tests\Support;
use App\Models\Setting;
trait InteractsWithSettings
trait InitializesSettings
{
protected Settings $settings;

View file

@ -9,7 +9,7 @@ use RuntimeException;
use Tests\Support\AssertsAgainstSlackNotifications;
use Tests\Support\CustomTestMacros;
use Tests\Support\InteractsWithAuthentication;
use Tests\Support\InteractsWithSettings;
use Tests\Support\InitializesSettings;
abstract class TestCase extends BaseTestCase
{
@ -17,6 +17,7 @@ abstract class TestCase extends BaseTestCase
use CreatesApplication;
use CustomTestMacros;
use InteractsWithAuthentication;
use InitializesSettings;
use LazilyRefreshDatabase;
private array $globallyDisabledMiddleware = [
@ -25,20 +26,23 @@ abstract class TestCase extends BaseTestCase
protected function setUp(): void
{
if (!file_exists(realpath(__DIR__ . '/../') . '/.env.testing')) {
throw new RuntimeException(
'.env.testing file does not exist. Aborting to avoid wiping your local database'
);
}
$this->guardAgainstMissingEnv();
parent::setUp();
$this->registerCustomMacros();
$this->withoutMiddleware($this->globallyDisabledMiddleware);
if (collect(class_uses_recursive($this))->contains(InteractsWithSettings::class)) {
$this->initializeSettings();
}
$this->initializeSettings();
}
$this->registerCustomMacros();
private function guardAgainstMissingEnv(): void
{
if (!file_exists(realpath(__DIR__ . '/../') . '/.env.testing')) {
throw new RuntimeException(
'.env.testing file does not exist. Aborting to avoid wiping your local database.'
);
}
}
}

View file

@ -2,14 +2,10 @@
namespace Tests\Unit;
use App\Models\AssetMaintenance;
use Carbon\Carbon;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetMaintenanceTest extends TestCase
{
use InteractsWithSettings;
public function testZerosOutWarrantyIfBlank()
{
$c = new AssetMaintenance;

View file

@ -4,13 +4,10 @@ namespace Tests\Unit;
use App\Models\Asset;
use App\Models\Category;
use App\Models\AssetModel;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetModelTest extends TestCase
{
use InteractsWithSettings;
public function testAnAssetModelContainsAssets()
{
$category = Category::factory()->create([

View file

@ -5,13 +5,10 @@ use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Category;
use Carbon\Carbon;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class AssetTest extends TestCase
{
use InteractsWithSettings;
public function testAutoIncrement()
{
$this->settings->enableAutoIncrement();

View file

@ -4,13 +4,10 @@ namespace Tests\Unit;
use App\Models\Category;
use App\Models\AssetModel;
use App\Models\Asset;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class CategoryTest extends TestCase
{
use InteractsWithSettings;
public function testFailsEmptyValidation()
{
// An Asset requires a name, a qty, and a category_id.

View file

@ -12,13 +12,10 @@ use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class CompanyScopingTest extends TestCase
{
use InteractsWithSettings;
public function models(): array
{
return [

View file

@ -5,13 +5,10 @@ use App\Models\Category;
use App\Models\Company;
use App\Models\Component;
use App\Models\Location;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class ComponentTest extends TestCase
{
use InteractsWithSettings;
public function testAComponentBelongsToACompany()
{
$component = Component::factory()

View file

@ -5,13 +5,10 @@ use App\Models\Depreciation;
use App\Models\Category;
use App\Models\License;
use App\Models\AssetModel;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class DepreciationTest extends TestCase
{
use InteractsWithSettings;
public function testADepreciationHasModels()
{
$depreciation = Depreciation::factory()->create();

View file

@ -3,8 +3,6 @@
namespace Tests\Unit;
use App\Models\Ldap;
use Exception;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
/**
@ -12,7 +10,6 @@ use Tests\TestCase;
*/
class LdapTest extends TestCase
{
use InteractsWithSettings;
use \phpmock\phpunit\PHPMock;
public function testConnect()

View file

@ -4,13 +4,10 @@ namespace Tests\Unit\Models\Company;
use App\Models\Company;
use App\Models\User;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class GetIdForCurrentUserTest extends TestCase
{
use InteractsWithSettings;
public function testReturnsProvidedValueWhenFullCompanySupportDisabled()
{
$this->settings->disableMultipleFullCompanySupport();

View file

@ -8,13 +8,10 @@ use App\Models\Category;
use Carbon\Carbon;
use App\Notifications\CheckoutAssetNotification;
use Illuminate\Support\Facades\Notification;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class NotificationTest extends TestCase
{
use InteractsWithSettings;
public function testAUserIsEmailedIfTheyCheckoutAnAssetWithEULA()
{
$admin = User::factory()->superuser()->create();

View file

@ -2,13 +2,10 @@
namespace Tests\Unit;
use App\Models\SnipeModel;
use Tests\Support\InteractsWithSettings;
use Tests\TestCase;
class SnipeModelTest extends TestCase
{
use InteractsWithSettings;
public function testSetsPurchaseDatesAppropriately()
{
$c = new SnipeModel;

View file

@ -183,11 +183,11 @@ mix
[
"./resources/assets/js/dragtable.js",
'./node_modules/bootstrap-table/dist/bootstrap-table.js',
"./resources/assets/js/bootstrap-table-reorder-columns.js",
'./node_modules/bootstrap-table/dist/extensions/mobile/bootstrap-table-mobile.js',
'./node_modules/bootstrap-table/dist/extensions/export/bootstrap-table-export.js',
'./node_modules/bootstrap-table/dist/extensions/cookie/bootstrap-table-cookie.js',
'./node_modules/bootstrap-table/dist/extensions/sticky-header/bootstrap-table-sticky-header.js',
'./node_modules/bootstrap-table/dist/extensions/addrbar/bootstrap-table-addrbar.js',
'./resources/assets/js/extensions/jquery.base64.js',
'./node_modules/tableexport.jquery.plugin/tableExport.min.js',
'./node_modules/tableexport.jquery.plugin/libs/jsPDF/jspdf.umd.min.js',