Merge branch 'develop' into use_new_laravel_scim_server
This commit is contained in:
@@ -28,6 +28,7 @@ jobs:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
- "8.5"
|
||||
|
||||
name: PHP ${{ matrix.php-version }}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ jobs:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
- "8.5"
|
||||
|
||||
|
||||
name: PHP ${{ matrix.php-version }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.3"
|
||||
- "8.5"
|
||||
|
||||
name: PHP ${{ matrix.php-version }}
|
||||
|
||||
|
||||
@@ -208,6 +208,30 @@ class AcceptanceController extends Controller
|
||||
'qty' => $acceptance->qty ?? 1,
|
||||
];
|
||||
|
||||
// Include asset custom fields that are explicitly allowed in outbound emails/PDFs.
|
||||
if ($item instanceof Asset && $item->model && $item->model->fieldset) {
|
||||
$customFields = [];
|
||||
$fields = $item->model->fieldset->fields
|
||||
->where('show_in_email', true)
|
||||
->where('field_encrypted', false);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$dbColumn = $field->db_column;
|
||||
$value = $item->{$dbColumn};
|
||||
|
||||
if (! is_null($value) && $value !== '') {
|
||||
$customFields[] = [
|
||||
'label' => $field->name,
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($customFields)) {
|
||||
$data['custom_fields'] = $customFields;
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->input('asset_acceptance') === 'accepted') {
|
||||
|
||||
$pdf_filename = 'accepted-'.$acceptance->checkoutable_id.'-'.$acceptance->display_checkoutable_type.'-eula-'.date('Y-m-d-h-i-s').'.pdf';
|
||||
|
||||
@@ -1164,7 +1164,7 @@ class AssetsController extends Controller
|
||||
'asset_tag' => e($asset->asset_tag),
|
||||
'audit_by_field' => e(Str::headline($audit_by_field)),
|
||||
'audit_key' => e($audit_key),
|
||||
'note' => e($request->input('note')),
|
||||
'note' => $request->filled('note') ? e($request->input('note')) : null,
|
||||
'status_label' => e($asset->status?->display_name),
|
||||
'status_type' => $asset->status?->getStatuslabelType(),
|
||||
'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date),
|
||||
@@ -1205,7 +1205,7 @@ class AssetsController extends Controller
|
||||
|
||||
// Validate the rest of the data before we turn off the event dispatcher
|
||||
if ($asset->isInvalid()) {
|
||||
return response()->json(Helper::formatStandardApiResponse('error', ['asset_tag' => $asset->asset_tag], $asset->getErrors()));
|
||||
return response()->json(Helper::formatStandardApiResponse('error', $payload, $asset->getErrors()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -433,7 +433,7 @@ class LoginController extends Controller
|
||||
$user->saveQuietly();
|
||||
$request->session()->put('2fa_authed', $user->id);
|
||||
|
||||
return redirect()->route('home')->with('success', trans('auth/message.signin.success'));
|
||||
return redirect()->intended()->with('success', trans('auth/message.signin.success'));
|
||||
}
|
||||
|
||||
return redirect()->route('two-factor')->with('error', trans('auth/message.two_factor.invalid_code'));
|
||||
|
||||
@@ -74,8 +74,7 @@ class SamlController extends Controller
|
||||
public function login(Request $request)
|
||||
{
|
||||
$auth = $this->saml->getAuth();
|
||||
$ssoUrl = $auth->login(null, [], false, false, false, false);
|
||||
|
||||
$ssoUrl = $auth->login(session()->get('url.intended'), [], false, false, false, false);
|
||||
return redirect()->away($ssoUrl);
|
||||
}
|
||||
|
||||
@@ -96,6 +95,7 @@ class SamlController extends Controller
|
||||
$saml = $this->saml;
|
||||
$auth = $saml->getAuth();
|
||||
$saml_exception = false;
|
||||
session()->put('url.intended', $request->post('RelayState'));
|
||||
try {
|
||||
$auth->processResponse();
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -46,6 +46,7 @@ class CheckForTwoFactor
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
redirect()->setIntendedUrl(url()->full()); // save the 'current' URL so we can send the user back to it?
|
||||
// Otherwise make sure they're enrolled and show them the 2FA code screen
|
||||
if ((auth()->user()->two_factor_secret != '') && (auth()->user()->two_factor_enrolled == '1')) {
|
||||
return redirect()->route('two-factor')->with('info', trans('auth/message.two_factor.enter_two_factor_code'));
|
||||
|
||||
@@ -57,6 +57,7 @@ class ImageUploadRequest extends Request
|
||||
* had it once to allow encoded image uploads.
|
||||
*/
|
||||
return [
|
||||
'avatar' => 'auto',
|
||||
'image' => 'auto',
|
||||
'image_source' => 'auto',
|
||||
];
|
||||
|
||||
@@ -58,10 +58,26 @@ class CheckinAssetMail extends BaseMailable
|
||||
{
|
||||
$this->item->load('status');
|
||||
$fields = [];
|
||||
$customFields = [];
|
||||
|
||||
// Check if the item has custom fields associated with it
|
||||
if (($this->item->model) && ($this->item->model->fieldset)) {
|
||||
$fields = $this->item->model->fieldset->fields;
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (! $field->show_in_email || $field->field_encrypted == '1') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->item->{$field->db_column_name()};
|
||||
|
||||
if (! is_null($value) && $value !== '') {
|
||||
$customFields[] = [
|
||||
'label' => $field->name,
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Content(
|
||||
@@ -73,6 +89,7 @@ class CheckinAssetMail extends BaseMailable
|
||||
'note' => $this->note,
|
||||
'target' => $this->target,
|
||||
'fields' => $fields,
|
||||
'custom_fields' => $customFields,
|
||||
'expected_checkin' => $this->expected_checkin,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -75,6 +75,7 @@ class CheckoutAssetMail extends BaseMailable
|
||||
$eula = method_exists($this->item, 'getEula') ? $this->item->getEula() : '';
|
||||
$req_accept = $this->requiresAcceptance();
|
||||
$fields = [];
|
||||
$customFields = [];
|
||||
$name = null;
|
||||
|
||||
if ($this->target instanceof User) {
|
||||
@@ -88,6 +89,21 @@ class CheckoutAssetMail extends BaseMailable
|
||||
// Check if the item has custom fields associated with it
|
||||
if (($this->item->model) && ($this->item->model->fieldset)) {
|
||||
$fields = $this->item->model->fieldset->fields;
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (! $field->show_in_email || $field->field_encrypted == '1') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->item->{$field->db_column_name()};
|
||||
|
||||
if (! is_null($value) && $value !== '') {
|
||||
$customFields[] = [
|
||||
'label' => $field->name,
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$accept_url = is_null($this->acceptance) ? null : route('account.accept.item', $this->acceptance);
|
||||
@@ -101,6 +117,7 @@ class CheckoutAssetMail extends BaseMailable
|
||||
'note' => $this->note,
|
||||
'target' => $name,
|
||||
'fields' => $fields,
|
||||
'custom_fields' => $customFields,
|
||||
'eula' => $eula,
|
||||
'req_accept' => $req_accept,
|
||||
'accept_url' => $accept_url,
|
||||
|
||||
+83
-18
@@ -9,9 +9,11 @@ use App\Presenters\ActionlogPresenter;
|
||||
use App\Presenters\Presentable;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
@@ -53,6 +55,13 @@ class Actionlog extends SnipeModel
|
||||
|
||||
use Searchable;
|
||||
|
||||
/**
|
||||
* Cache whether a model table has a company_id column.
|
||||
*
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
protected static array $companyColumnCache = [];
|
||||
|
||||
/**
|
||||
* The attributes that should be included when searching the model.
|
||||
*
|
||||
@@ -116,25 +125,81 @@ class Actionlog extends SnipeModel
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
static::creating(
|
||||
function (self $actionlog) {
|
||||
// If the admin is a superadmin, let's see if the target instead has a company.
|
||||
if (auth()->user() && auth()->user()->isSuperUser()) {
|
||||
if ($actionlog->target) {
|
||||
$actionlog->company_id = $actionlog->target->company_id;
|
||||
} elseif ($actionlog->item) {
|
||||
$actionlog->company_id = $actionlog->item->company_id;
|
||||
}
|
||||
} elseif (auth()->user() && auth()->user()->company) {
|
||||
$actionlog->company_id = auth()->user()->company_id;
|
||||
}
|
||||
|
||||
if ($actionlog->action_date == '') {
|
||||
$actionlog->action_date = Carbon::now();
|
||||
}
|
||||
|
||||
static::creating(function (self $actionlog): void {
|
||||
// Only resolve company_id if it was never explicitly set by the caller.
|
||||
// Using array_key_exists on getRawOriginal() / getAttributes() lets us
|
||||
// distinguish "was set to null intentionally" from "was never set at all".
|
||||
if (! array_key_exists('company_id', $actionlog->getAttributes())) {
|
||||
$actionlog->company_id = static::resolveCompanyIdFromAttributes(
|
||||
$actionlog->target_type,
|
||||
$actionlog->target_id,
|
||||
$actionlog->item_type,
|
||||
$actionlog->item_id,
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if ($actionlog->action_date == '') {
|
||||
$actionlog->action_date = Carbon::now();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the company_id for a new action log by querying the item model
|
||||
* directly, bypassing all global scopes to avoid FMCS filtering issues.
|
||||
*
|
||||
* We intentionally prefer the item (asset, license, etc.) over the target
|
||||
* (user, location) because FMCS visibility is based on who *owns* the item,
|
||||
* not who it was checked out to. If the item has no company_id we fall back
|
||||
* to the target so that logs on unowned items still get a company stamp where
|
||||
* possible.
|
||||
*
|
||||
* This has to include an exception for the asset models table, since they are
|
||||
* not company-constrained (on purpose.)
|
||||
*/
|
||||
protected static function resolveCompanyIdFromAttributes(
|
||||
?string $targetType,
|
||||
?int $targetId,
|
||||
?string $itemType,
|
||||
?int $itemId,
|
||||
): ?int {
|
||||
// Prefer the item (the thing being acted upon) for FMCS ownership.
|
||||
$companyId = static::resolveCompanyIdFromModelClass($itemType, $itemId);
|
||||
|
||||
if ($companyId !== null) {
|
||||
return $companyId;
|
||||
}
|
||||
|
||||
// Fall back to target only when the item has no company_id.
|
||||
return static::resolveCompanyIdFromModelClass($targetType, $targetId);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve company_id from a model class and ID, but only if that model's
|
||||
* table has a company_id column.
|
||||
*/
|
||||
protected static function resolveCompanyIdFromModelClass(?string $modelClass, ?int $id): ?int
|
||||
{
|
||||
if (! $modelClass || ! $id || ! class_exists($modelClass) || ! is_subclass_of($modelClass, Model::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var Model $instance */
|
||||
$instance = app($modelClass);
|
||||
$table = $instance->getTable();
|
||||
|
||||
$hasCompanyColumn = static::$companyColumnCache[$table]
|
||||
??= Schema::hasColumn($table, 'company_id');
|
||||
|
||||
if (! $hasCompanyColumn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $modelClass::withoutGlobalScopes()
|
||||
->whereKey($id)
|
||||
->value('company_id');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -243,13 +243,27 @@ class CheckoutAcceptance extends Model
|
||||
if ($data['item_serial'] != null) {
|
||||
$pdf->writeHTML(trans('admin/hardware/form.serial').': '.e($data['item_serial']), true, 0, true, 0, '');
|
||||
}
|
||||
if (!empty($data['custom_fields']) && is_iterable($data['custom_fields'])) {
|
||||
foreach ($data['custom_fields'] as $customField) {
|
||||
$label = $customField['label'] ?? null;
|
||||
$value = $customField['value'] ?? null;
|
||||
|
||||
if (($label !== null) && ($value !== null) && ($value !== '')) {
|
||||
$pdf->writeHTML(e((string) $label) . ': ' . e((string) $value), true, 0, true, 0, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (($data['qty'] != null) && ($data['qty'] > 1)) {
|
||||
$pdf->writeHTML(trans('general.qty').': '.e($data['qty']), true, 0, true, 0, '');
|
||||
}
|
||||
$pdf->Ln();
|
||||
$pdf->writeHTML('<hr>', true, 0, true, 0, '');
|
||||
$pdf->writeHTML(trans('general.assignee').': '.e($data['assigned_to']).($data['employee_num'] ? ' ('.$data['employee_num'].')' : ''), true, 0, true, 0, '');
|
||||
if ($data['email'] != null) {
|
||||
$pdf->writeHTML(trans('general.email').': '.e($data['email']), true, 0, true, 0, '');
|
||||
}
|
||||
|
||||
$pdf->Ln();
|
||||
$pdf->writeHTML('<hr>', true, 0, true, 0, '');
|
||||
|
||||
|
||||
@@ -177,6 +177,7 @@ trait Loggable
|
||||
$log->note = $note;
|
||||
$log->action_date = $action_date;
|
||||
$log->quantity = $quantity;
|
||||
$log->company_id = $this->resolveLoggableCompanyId();
|
||||
|
||||
$changed = [];
|
||||
$array_to_flip = array_keys($fields_array);
|
||||
@@ -221,6 +222,22 @@ trait Loggable
|
||||
return $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the company_id that should be stamped on an action log entry.
|
||||
*
|
||||
* LicenseSeat does not carry a company_id directly — it belongs to a License,
|
||||
* so we fetch the parent license's company_id in that case. All other models
|
||||
* that use the Loggable trait have a company_id column directly.
|
||||
*/
|
||||
private function resolveLoggableCompanyId(): ?int
|
||||
{
|
||||
if (static::class === LicenseSeat::class) {
|
||||
return $this->license?->company_id;
|
||||
}
|
||||
|
||||
return $this->company_id ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Daniel Meltzer <dmeltzer.devel@gmail.com>
|
||||
*
|
||||
@@ -267,6 +284,7 @@ trait Loggable
|
||||
$log->location_id = null;
|
||||
$log->note = $note;
|
||||
$log->action_date = $action_date;
|
||||
$log->company_id = $this->resolveLoggableCompanyId();
|
||||
|
||||
if (! $action_date) {
|
||||
$log->action_date = date('Y-m-d H:i:s');
|
||||
@@ -383,6 +401,8 @@ trait Loggable
|
||||
$log->created_by = auth()->id();
|
||||
$log->filename = $filename;
|
||||
$log->action_date = date('Y-m-d H:i:s');
|
||||
// Explicitly stamp company_id from the item being audited so FMCS scoping works correctly.
|
||||
$log->company_id = $this->resolveLoggableCompanyId();
|
||||
$log->logaction('audit');
|
||||
|
||||
$params = [
|
||||
@@ -468,6 +488,7 @@ trait Loggable
|
||||
$log->action_date = date('Y-m-d H:i:s');
|
||||
$log->note = $note;
|
||||
$log->created_by = $created_by;
|
||||
$log->company_id = $this->resolveLoggableCompanyId();
|
||||
$log->logaction('create');
|
||||
$log->save();
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ class AcceptanceItemAcceptedNotification extends Notification
|
||||
$this->file = $params['file'] ?? null;
|
||||
$this->qty = $params['qty'] ?? null;
|
||||
$this->note = $params['note'] ?? null;
|
||||
$this->custom_fields = $params['custom_fields'] ?? [];
|
||||
|
||||
}
|
||||
|
||||
@@ -76,6 +77,7 @@ class AcceptanceItemAcceptedNotification extends Notification
|
||||
'assigned_to' => $this->assigned_to,
|
||||
'company_name' => $this->company_name,
|
||||
'qty' => $this->qty,
|
||||
'custom_fields' => $this->custom_fields,
|
||||
'intro_text' => trans('mail.acceptance_accepted_greeting', ['user' => $this->assigned_to, 'item' => $this->item_name]),
|
||||
])
|
||||
->subject('✅ '.trans('mail.acceptance_accepted', ['user' => $this->assigned_to, 'item' => $this->item_name]))
|
||||
|
||||
@@ -34,6 +34,7 @@ class AcceptanceItemAcceptedToUserNotification extends Notification
|
||||
$this->settings = Setting::getSettings();
|
||||
$this->file = $params['file'] ?? null;
|
||||
$this->qty = $params['qty'] ?? null;
|
||||
$this->custom_fields = $params['custom_fields'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,6 +73,7 @@ class AcceptanceItemAcceptedToUserNotification extends Notification
|
||||
'assigned_to' => $this->assigned_to,
|
||||
'company_name' => $this->company_name,
|
||||
'qty' => $this->qty,
|
||||
'custom_fields' => $this->custom_fields,
|
||||
'intro_text' => trans_choice('mail.acceptance_asset_accepted_to_user', $this->qty, ['qty' => $this->qty, 'site_name' => $this->settings->site_name]),
|
||||
])
|
||||
->attach($pdf_path)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Presenters;
|
||||
|
||||
use App\Models\CustomField;
|
||||
|
||||
final class CustomFieldPresenter
|
||||
{
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function visibilityIconsArray(CustomField $field): array
|
||||
{
|
||||
$icons = [];
|
||||
|
||||
if ($field->display_checkout) {
|
||||
$label = e(trans('admin/custom_fields/general.display_checkout'));
|
||||
$icons[] = '<span title="'.$label.'" data-tooltip="true"><i class="fa-solid fa-rotate-left text-muted" aria-hidden="true"></i><span class="sr-only">'.$label.'</span></span>';
|
||||
}
|
||||
|
||||
if ($field->display_checkin) {
|
||||
$label = e(trans('admin/custom_fields/general.display_checkin'));
|
||||
$icons[] = '<span title="'.$label.'" data-tooltip="true"><i class="fa-solid fa-rotate-right text-muted" aria-hidden="true"></i><span class="sr-only">'.$label.'</span></span>';
|
||||
}
|
||||
|
||||
if ($field->display_audit) {
|
||||
$label = e(trans('admin/custom_fields/general.display_audit'));
|
||||
$icons[] = '<span title="'.$label.'" data-tooltip="true"><i class="fas fa-clipboard-check text-muted" aria-hidden="true"></i><span class="sr-only">'.$label.'</span></span>';
|
||||
}
|
||||
|
||||
if ($field->display_in_user_view) {
|
||||
$label = e(trans('admin/custom_fields/general.display_in_user_view_table'));
|
||||
$icons[] = '<span title="'.$label.'" data-tooltip="true"><i class="fas fa-user text-muted" aria-hidden="true"></i><span class="sr-only">'.$label.'</span></span>';
|
||||
}
|
||||
|
||||
if ($field->show_in_listview) {
|
||||
$label = e(trans('admin/custom_fields/general.show_in_listview_short'));
|
||||
$icons[] = '<span title="'.$label.'" data-tooltip="true"><i class="fas fa-list text-muted" aria-hidden="true"></i><span class="sr-only">'.$label.'</span></span>';
|
||||
}
|
||||
|
||||
if ($field->show_in_email) {
|
||||
$label = e(trans('admin/custom_fields/general.show_in_email_short'));
|
||||
$icons[] = '<span title="'.$label.'" data-tooltip="true"><i class="fas fa-envelope text-muted" aria-hidden="true"></i><span class="sr-only">'.$label.'</span></span>';
|
||||
}
|
||||
|
||||
if ($field->show_in_requestable_list) {
|
||||
$label = e(trans('admin/custom_fields/general.show_in_requestable_list_short'));
|
||||
$icons[] = '<span title="'.$label.'" data-tooltip="true"><i class="fa-solid fa-bell-concierge text-muted" aria-hidden="true"></i><span class="sr-only">'.$label.'</span></span>';
|
||||
}
|
||||
|
||||
return $icons;
|
||||
}
|
||||
|
||||
public static function visibilityIcons(CustomField $field): string
|
||||
{
|
||||
return implode(' ', self::visibilityIconsArray($field));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -60,7 +60,7 @@
|
||||
"paragonie/constant_time_encoding": "^2.3",
|
||||
"paragonie/sodium_compat": "^1.19",
|
||||
"phpdocumentor/reflection-docblock": "^5.1",
|
||||
"phpspec/prophecy": "^1.10",
|
||||
"phpspec/prophecy": "^1.26",
|
||||
"pragmarx/google2fa-laravel": "^1.3",
|
||||
"rollbar/rollbar-laravel": "^8.0",
|
||||
"spatie/laravel-backup": "^9.0",
|
||||
|
||||
Generated
+15
-15
@@ -4,7 +4,6 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "010e4f225586a6b3e7bc98f2f71cc0c6",
|
||||
"packages": [
|
||||
{
|
||||
"name": "alek13/slack",
|
||||
@@ -6071,30 +6070,31 @@
|
||||
},
|
||||
{
|
||||
"name": "phpspec/prophecy",
|
||||
"version": "v1.22.0",
|
||||
"version": "v1.26.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpspec/prophecy.git",
|
||||
"reference": "35f1adb388946d92e6edab2aa2cb2b60e132ebd5"
|
||||
"reference": "09c2e5949d676286358a62af818f8407167a9dd6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/35f1adb388946d92e6edab2aa2cb2b60e132ebd5",
|
||||
"reference": "35f1adb388946d92e6edab2aa2cb2b60e132ebd5",
|
||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/09c2e5949d676286358a62af818f8407167a9dd6",
|
||||
"reference": "09c2e5949d676286358a62af818f8407167a9dd6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/instantiator": "^1.2 || ^2.0",
|
||||
"php": "^7.4 || 8.0.* || 8.1.* || 8.2.* || 8.3.* || 8.4.*",
|
||||
"phpdocumentor/reflection-docblock": "^5.2",
|
||||
"sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
|
||||
"sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
|
||||
"php": "8.2.* || 8.3.* || 8.4.* || 8.5.*",
|
||||
"phpdocumentor/reflection-docblock": "^5.2 || ^6.0",
|
||||
"sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0",
|
||||
"sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0",
|
||||
"symfony/deprecation-contracts": "^2.5 || ^3.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.40",
|
||||
"phpspec/phpspec": "^6.0 || ^7.0",
|
||||
"phpstan/phpstan": "^2.1.13",
|
||||
"phpunit/phpunit": "^8.0 || ^9.0 || ^10.0"
|
||||
"php-cs-fixer/shim": "^3.93.1",
|
||||
"phpspec/phpspec": "^6.0 || ^7.0 || ^8.0",
|
||||
"phpstan/phpstan": "^2.1.13, <2.1.34 || ^2.1.39",
|
||||
"phpunit/phpunit": "^11.0 || ^12.0 || ^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -6135,9 +6135,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpspec/prophecy/issues",
|
||||
"source": "https://github.com/phpspec/prophecy/tree/v1.22.0"
|
||||
"source": "https://github.com/phpspec/prophecy/tree/v1.26.1"
|
||||
},
|
||||
"time": "2025-04-29T14:58:06+00:00"
|
||||
"time": "2026-04-13T14:35:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'app_version' => 'v8.4.1',
|
||||
'full_app_version' => 'v8.4.1 - build 22183-g5898205480',
|
||||
'build_version' => '22183',
|
||||
'app_version' => 'v8.5.0-pre',
|
||||
'full_app_version' => 'v8.5.0-pre - build 22392-g5014b1c459',
|
||||
'build_version' => '22392',
|
||||
'prerelease_version' => '',
|
||||
'hash_version' => 'g5898205480',
|
||||
'full_hash' => 'v8.4.1-901-g5898205480',
|
||||
'hash_version' => 'g5014b1c459',
|
||||
'full_hash' => 'v8.5.0-pre-207-g5014b1c459',
|
||||
'branch' => 'develop',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Backfill action_logs.company_id only for legacy asset audits where the
|
||||
* value is currently NULL.
|
||||
*
|
||||
* Audits are only recorded on assets, so this migration intentionally scopes
|
||||
* to action_type='audit' and item_type=App\Models\Asset.
|
||||
*
|
||||
* Rows whose asset genuinely has no company (assets.company_id IS NULL) are
|
||||
* left as NULL.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
private const ASSET_CLASS = 'App\\Models\\Asset';
|
||||
|
||||
private const AUDIT_ACTION = 'audit';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$this->updateAssetAuditLogs(DB::getDriverName());
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// This backfill is intentionally non-reversible — we cannot know which
|
||||
// rows were NULL before the migration ran vs which were backfilled.
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp company_id for legacy audit rows tied to assets.
|
||||
*/
|
||||
private function updateAssetAuditLogs(string $driver): void
|
||||
{
|
||||
if ($driver === 'mysql' || $driver === 'mariadb') {
|
||||
// MySQL/MariaDB supports UPDATE ... JOIN directly
|
||||
DB::statement('
|
||||
UPDATE action_logs al
|
||||
INNER JOIN assets src
|
||||
ON src.id = al.item_id
|
||||
AND src.company_id IS NOT NULL
|
||||
SET al.company_id = src.company_id
|
||||
WHERE al.action_type = ?
|
||||
AND al.item_type = ?
|
||||
AND al.company_id IS NULL
|
||||
AND al.deleted_at IS NULL
|
||||
', [self::AUDIT_ACTION, self::ASSET_CLASS]);
|
||||
} else {
|
||||
// SQLite / PostgreSQL: use a correlated subquery update
|
||||
DB::statement('
|
||||
UPDATE action_logs
|
||||
SET company_id = (
|
||||
SELECT src.company_id
|
||||
FROM assets src
|
||||
WHERE src.id = action_logs.item_id
|
||||
AND src.company_id IS NOT NULL
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE action_type = ?
|
||||
AND item_type = ?
|
||||
AND company_id IS NULL
|
||||
AND deleted_at IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM assets src2
|
||||
WHERE src2.id = action_logs.item_id
|
||||
AND src2.company_id IS NOT NULL
|
||||
)
|
||||
', [self::AUDIT_ACTION, self::ASSET_CLASS]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -13,7 +13,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -479,6 +479,9 @@
|
||||
<th data-switchable="true" data-visible="true">
|
||||
{{ trans('admin/hardware/table.serial') }}
|
||||
</th>
|
||||
<th data-switchable="true" data-visible="true">
|
||||
{{ trans('general.manufacturer') }}
|
||||
</th>
|
||||
<th data-switchable="true" data-visible="false">
|
||||
{{ trans('admin/hardware/form.default_location') }}
|
||||
</th>
|
||||
@@ -552,6 +555,11 @@
|
||||
<td>
|
||||
{{ $asset->serial }}
|
||||
</td>
|
||||
<td>
|
||||
@if (($asset->model) && ($asset->model->manufacturer))
|
||||
{!! $asset->model->manufacturer->present()->formattedNameLink !!}
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
{!! ($asset->defaultLoc) ? $asset->defaultLoc->present()->formattedNameLink : '' !!}
|
||||
|
||||
|
||||
@@ -57,21 +57,32 @@
|
||||
<fieldset name="login" aria-label="login">
|
||||
|
||||
<div class="form-group{{ $errors->has('username') ? ' has-error' : '' }}">
|
||||
<label for="username">
|
||||
<label for="username" class="control-label">
|
||||
<x-icon type="user" />
|
||||
{{ trans('admin/users/table.username') }}
|
||||
</label>
|
||||
<input class="form-control" placeholder="{{ trans('admin/users/table.username') }}" name="username" type="text" id="username" autocomplete="{{ (config('auth.login_autocomplete') === true) ? 'on' : 'off' }}" autofocus>
|
||||
<input class="form-control" placeholder="{{ trans('admin/users/table.username') }}" name="username" type="text" id="username" autocomplete="{{ (config('auth.login_autocomplete') === true) ? 'on' : 'off' }}" autocapitalize="off" spellcheck="false" autofocus>
|
||||
{!! $errors->first('username', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}">
|
||||
<label for="password">
|
||||
<label for="password" class="control-label">
|
||||
<x-icon type="password" />
|
||||
{{ trans('admin/users/table.password') }}
|
||||
</label>
|
||||
<input class="form-control" placeholder="{{ trans('admin/users/table.password') }}" name="password" type="password" id="password" autocomplete="{{ (config('auth.login_autocomplete') === true) ? 'on' : 'off' }}">
|
||||
|
||||
<div class="input-group">
|
||||
<input class="form-control" placeholder="{{ trans('admin/users/table.password') }}" name="password" type="password" id="password-field" autocomplete="{{ (config('auth.login_autocomplete') === true) ? 'on' : 'off' }}" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
<span class="input-group-addon">
|
||||
<i data-toggle="#password-field" class="fa fa-fw fa-eye toggle-password" aria-hidden="true"></i>
|
||||
<span class="sr-only">Toggle password visibility</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!! $errors->first('password', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-control">
|
||||
<input name="remember" type="checkbox" value="1" id="remember"> {{ trans('auth/general.remember_me') }}
|
||||
|
||||
@@ -1 +1,19 @@
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right hidden-xs" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
@props([
|
||||
'hideOnXs' => false,
|
||||
])
|
||||
|
||||
<button
|
||||
type="button"
|
||||
id="expand-info-panel-button"
|
||||
data-tooltip="true"
|
||||
title="{{ trans('button.show_hide_info') }}"
|
||||
aria-label="{{ trans('button.show_hide_info') }}"
|
||||
style="background: none; border: 0; padding: 0;"
|
||||
{{ $attributes->class([
|
||||
'fa-regular',
|
||||
'fa-2x',
|
||||
'fa-square-caret-right',
|
||||
'pull-right',
|
||||
'hidden-xs' => $hideOnXs,
|
||||
]) }}
|
||||
></button>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@endsection
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="form-group {{ $errors->has('audit_by_field') ? 'error' : '' }}">
|
||||
<label for="audit_by_field" class="col-md-3 control-label" id="audit_by_field">{{ trans('general.audit_by_field') }}</label>
|
||||
<div class="col-md-8">
|
||||
<select name="audit_by_field" data-minimum-results-for-search="Infinity" id="audit_by_field" class="form-control select2" aria-label="audit_by_field" required>
|
||||
<select name="audit_by_field" data-minimum-results-for-search="Infinity" id="audit_by_field" style="width: 100% !important" class="form-control select2" aria-label="audit_by_field" required>
|
||||
<option value="asset_tag">{{ trans('general.asset_tag') }}</option>
|
||||
<option value="serial" {{ (($settings->unique_serial != '1') ? 'disabled' : '') }}>{{ trans('general.serial_number') }}</option>
|
||||
</select>
|
||||
@@ -45,10 +45,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Tag/Serial -->
|
||||
<div class="form-group {{ $errors->has('asset_tag') ? 'error' : '' }}">
|
||||
<div class="form-group {{ $errors->has('audit_key') ? 'error' : '' }}">
|
||||
<label for="audit_key" class="col-md-3 control-label" id="audit_key_label">{{ trans('general.asset_tag') }}</label>
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="audit_key" required value="{{ old('audit_key') }}">
|
||||
<input type="text" class="form-control" name="audit_key" id="audit_key" required
|
||||
value="{{ old('audit_key') }}">
|
||||
{!! $errors->first('audit_key', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<x-button.info-panel-toggle/>
|
||||
<x-button.info-panel-toggle hide-on-xs/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
@@ -240,7 +240,7 @@
|
||||
@endif
|
||||
|
||||
|
||||
@if($asset->purchase_date || $asset->asset_eol_date || $asset->depreciated_date() || $asset->warranty_expires)
|
||||
@if(($asset->purchase_date && $asset->asset_eol_date) || $asset->depreciated_date() || $asset->warranty_expires)
|
||||
<x-well class="well-sm">
|
||||
@if($asset->purchase_date && $asset->asset_eol_date)
|
||||
<x-progressbar use_well="false" columns="12" text="{{ trans('general.device_eol') }}" :percent="$asset->eolProgressPercent()">
|
||||
|
||||
@@ -64,7 +64,18 @@
|
||||
{{-- Javascript files --}}
|
||||
<script src="{{ url(mix('js/dist/all.js')) }}" nonce="{{ csrf_token() }}"></script>
|
||||
|
||||
|
||||
<script>
|
||||
$(".toggle-password").click(function () {
|
||||
$(this).toggleClass("fa-eye fa-eye-slash");
|
||||
var input = $($(this).attr("data-toggle"));
|
||||
if (input.attr("type") === "password") {
|
||||
input.attr("type", "text");
|
||||
}
|
||||
else {
|
||||
input.attr("type", "password");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@stack('js')
|
||||
</body>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
|
||||
|
||||
@@ -37,11 +37,13 @@
|
||||
@if (isset($status))
|
||||
| **{{ trans('general.status') }}** | {{ $status }} |
|
||||
@endif
|
||||
@foreach($fields as $field)
|
||||
@if (($item->{ $field->db_column_name() }!='') && ($field->show_in_email) && ($field->field_encrypted=='0'))
|
||||
| **{{ $field->name }}** | {{ $item->{ $field->db_column_name() } }} |
|
||||
@if (!empty($custom_fields))
|
||||
@foreach($custom_fields as $customField)
|
||||
@if (!empty($customField['label']) && array_key_exists('value', $customField) && $customField['value'] !== '')
|
||||
| **{{ $customField['label'] }}** | {{ $customField['value'] }} |
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@if ($admin)
|
||||
| **{{ trans('general.administrator') }}** | {{ $admin->display_name }} |
|
||||
@endif
|
||||
|
||||
@@ -40,11 +40,13 @@
|
||||
@if ((isset($expected_checkin)) && ($expected_checkin!=''))
|
||||
| **{{ trans('mail.expecting_checkin_date') }}** | {{ $expected_checkin }} |
|
||||
@endif
|
||||
@foreach($fields as $field)
|
||||
@if (($item->{ $field->db_column_name() }!='') && ($field->show_in_email) && ($field->field_encrypted=='0'))
|
||||
| **{{ $field->name }}** | {{ $item->{ $field->db_column_name() } }} |
|
||||
@if (!empty($custom_fields))
|
||||
@foreach($custom_fields as $customField)
|
||||
@if (!empty($customField['label']) && array_key_exists('value', $customField) && $customField['value'] !== '')
|
||||
| **{{ $customField['label'] }}** | {{ $customField['value'] }} |
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@if ($admin)
|
||||
| **{{ trans('general.administrator') }}** | {{ $admin->display_name }} |
|
||||
@endif
|
||||
|
||||
@@ -10,7 +10,7 @@ use Carbon\Carbon;
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
||||
@@ -84,8 +84,19 @@
|
||||
|
||||
@endif
|
||||
|
||||
@if ($field->help_text!='')
|
||||
<p class="help-block">{{ $field->help_text }}</p>
|
||||
@if (count(\App\Presenters\CustomFieldPresenter::visibilityIconsArray($field)) > 0)
|
||||
@if ($field->help_text != '')
|
||||
<p class="help-block">
|
||||
{{ $field->help_text }}
|
||||
<br>{!! \App\Presenters\CustomFieldPresenter::visibilityIcons($field) !!}
|
||||
</p>
|
||||
@else
|
||||
<div class="help-block">
|
||||
{!! \App\Presenters\CustomFieldPresenter::visibilityIcons($field) !!}
|
||||
</div>
|
||||
@endif
|
||||
@elseif ($field->help_text != '')
|
||||
<p class="help-block">{{ $field->help_text }}</p>
|
||||
@endif
|
||||
|
||||
<?php
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -43,6 +43,13 @@
|
||||
@if (isset($qty))
|
||||
| **{{ trans('general.qty') }}** | {{ $qty }} |
|
||||
@endif
|
||||
@if (!empty($custom_fields))
|
||||
@foreach($custom_fields as $customField)
|
||||
@if (!empty($customField['label']) && array_key_exists('value', $customField) && $customField['value'] !== '')
|
||||
| **{{ $customField['label'] }}** | {{ $customField['value'] }} |
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@endcomponent
|
||||
|
||||
{{ trans('mail.best_regards') }}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<i class="fa-regular fa-2x fa-square-caret-right pull-right" id="expand-info-panel-button" data-tooltip="true" title="{{ trans('button.show_hide_info') }}"></i>
|
||||
<x-button.info-panel-toggle/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -612,7 +612,7 @@
|
||||
|
||||
@foreach ($groups as $id => $group)
|
||||
<option value="{{ $id }}"
|
||||
{{ ($userGroups->keys()->contains($id) ? ' selected="selected"' : '') }}>
|
||||
{{ ($userGroups->keys()->contains($id) ? ' selected' : '') }}>
|
||||
{{ $group }}
|
||||
</option>
|
||||
@endforeach
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
@stop
|
||||
|
||||
@section('header_right')
|
||||
<x-button.info-panel-toggle/>
|
||||
<x-button.info-panel-toggle hide-on-xs/>
|
||||
@endsection
|
||||
|
||||
{{-- Page content --}}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Http\Controllers\Api;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Laravel\Passport\Client;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -25,6 +26,23 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu
|
||||
], 404);
|
||||
});
|
||||
|
||||
Route::withoutMiddleware(['api'])->get('/client', function () {
|
||||
$client = Client::firstOrCreate(
|
||||
['redirect' => 'com.grokability.snipeitmobile://home'],
|
||||
[
|
||||
'name' => 'Snipe-IT Mobile App',
|
||||
'user_id' => null,
|
||||
'secret' => '',
|
||||
'personal_access_client' => false,
|
||||
'password_client' => false,
|
||||
'revoked' => false,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Account routes
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\ActionLogs;
|
||||
|
||||
use App\Models\Asset;
|
||||
use App\Models\Company;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Verifies the backfill migration logic that stamps action_logs.company_id
|
||||
* for legacy asset audit rows where the company is currently NULL.
|
||||
*
|
||||
* Rather than running the migration class directly (which would conflict with
|
||||
* LazilyRefreshDatabase), we replicate the exact UPDATE SQL used by the
|
||||
* migration and assert on the resulting rows.
|
||||
*/
|
||||
class ActionlogCompanyIdBackfillTest extends TestCase
|
||||
{
|
||||
private const ASSET_CLASS = 'App\\Models\\Asset';
|
||||
|
||||
private const AUDIT_ACTION = 'audit';
|
||||
|
||||
/**
|
||||
* Insert an action_log row bypassing Eloquent so that company_id stays NULL,
|
||||
* simulating a historical record written before FMCS stamping was added.
|
||||
*/
|
||||
private function insertLegacyLog(array $attributes): int
|
||||
{
|
||||
return DB::table('action_logs')->insertGetId(array_merge([
|
||||
'action_type' => self::AUDIT_ACTION,
|
||||
'item_type' => self::ASSET_CLASS,
|
||||
'item_id' => null,
|
||||
'company_id' => null,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the same UPDATE logic the migration uses.
|
||||
*/
|
||||
private function runBackfill(): void
|
||||
{
|
||||
$driver = DB::getDriverName();
|
||||
|
||||
if ($driver === 'mysql' || $driver === 'mariadb') {
|
||||
DB::statement('
|
||||
UPDATE action_logs al
|
||||
INNER JOIN assets src ON src.id = al.item_id AND src.company_id IS NOT NULL
|
||||
SET al.company_id = src.company_id
|
||||
WHERE al.action_type = ?
|
||||
AND al.item_type = ?
|
||||
AND al.company_id IS NULL
|
||||
AND al.deleted_at IS NULL
|
||||
', [self::AUDIT_ACTION, self::ASSET_CLASS]);
|
||||
} else {
|
||||
DB::statement('
|
||||
UPDATE action_logs
|
||||
SET company_id = (
|
||||
SELECT src.company_id FROM assets src
|
||||
WHERE src.id = action_logs.item_id AND src.company_id IS NOT NULL
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE action_type = ?
|
||||
AND item_type = ?
|
||||
AND company_id IS NULL
|
||||
AND deleted_at IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM assets src2
|
||||
WHERE src2.id = action_logs.item_id AND src2.company_id IS NOT NULL
|
||||
)
|
||||
', [self::AUDIT_ACTION, self::ASSET_CLASS]);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public function test_backfill_populates_company_id_for_asset_audit(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$asset = Asset::factory()->create(['company_id' => $company->id]);
|
||||
|
||||
$logId = $this->insertLegacyLog(['item_type' => self::ASSET_CLASS, 'item_id' => $asset->id]);
|
||||
|
||||
$this->runBackfill();
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'id' => $logId,
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_backfill_does_not_overwrite_existing_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$otherCompany = Company::factory()->create();
|
||||
$asset = Asset::factory()->create(['company_id' => $otherCompany->id]);
|
||||
|
||||
// Row already has a company_id — the backfill must leave it alone
|
||||
$logId = $this->insertLegacyLog([
|
||||
'item_type' => self::ASSET_CLASS,
|
||||
'item_id' => $asset->id,
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
|
||||
$this->runBackfill();
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'id' => $logId,
|
||||
'company_id' => $company->id, // unchanged
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_backfill_leaves_null_when_item_has_no_company(): void
|
||||
{
|
||||
$asset = Asset::factory()->create(['company_id' => null]);
|
||||
|
||||
$logId = $this->insertLegacyLog(['item_type' => self::ASSET_CLASS, 'item_id' => $asset->id]);
|
||||
|
||||
$this->runBackfill();
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'id' => $logId,
|
||||
'company_id' => null, // item has no company, so log stays null
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_backfill_ignores_non_audit_action_logs(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$asset = Asset::factory()->create(['company_id' => $company->id]);
|
||||
|
||||
$logId = $this->insertLegacyLog([
|
||||
'action_type' => 'checkout',
|
||||
'item_type' => self::ASSET_CLASS,
|
||||
'item_id' => $asset->id,
|
||||
]);
|
||||
|
||||
$this->runBackfill();
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'id' => $logId,
|
||||
'company_id' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\ActionLogs;
|
||||
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Company;
|
||||
use App\Models\Component;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\Location;
|
||||
use App\Models\Statuslabel;
|
||||
use App\Models\User;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Confirms that action_logs.company_id is correctly stamped for every
|
||||
* logged event so that FMCS scoping works correctly.
|
||||
*
|
||||
* Each test creates an item belonging to a specific company and triggers the
|
||||
* relevant action, then asserts that the resulting action log row carries the
|
||||
* same company_id as the item.
|
||||
*/
|
||||
class ActionlogCompanyIdTest extends TestCase
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Asset events
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function test_asset_audit_log_stores_the_assets_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$asset = Asset::factory()->create(['company_id' => $company->id]);
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.asset.audit', $asset), ['note' => 'audit test'])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Asset::class,
|
||||
'item_id' => $asset->id,
|
||||
'action_type' => 'audit',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asset_checkout_to_user_log_stores_the_assets_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$asset = Asset::factory()->create(['company_id' => $company->id]);
|
||||
$user = User::factory()->create();
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.asset.checkout', $asset), [
|
||||
'checkout_to_type' => 'user',
|
||||
'assigned_user' => $user->id,
|
||||
'status_id' => $asset->status_id,
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Asset::class,
|
||||
'item_id' => $asset->id,
|
||||
'action_type' => 'checkout',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asset_checkout_to_location_log_stores_the_assets_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$asset = Asset::factory()->create(['company_id' => $company->id]);
|
||||
$location = Location::factory()->create();
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.asset.checkout', $asset), [
|
||||
'checkout_to_type' => 'location',
|
||||
'assigned_location' => $location->id,
|
||||
'status_id' => $asset->status_id,
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Asset::class,
|
||||
'item_id' => $asset->id,
|
||||
'action_type' => 'checkout',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asset_checkin_log_stores_the_assets_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$asset = Asset::factory()->assignedToUser()->create(['company_id' => $company->id]);
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.asset.checkin', $asset))
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Asset::class,
|
||||
'item_id' => $asset->id,
|
||||
'action_type' => 'checkin from',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asset_create_log_stores_the_assets_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$admin = User::factory()->superuser()->create();
|
||||
$model = AssetModel::factory()->create();
|
||||
$status = Statuslabel::factory()->readyToDeploy()->create();
|
||||
$tag = 'COMPANY-ID-TEST-'.uniqid();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.assets.store'), [
|
||||
'asset_tag' => $tag,
|
||||
'model_id' => $model->id,
|
||||
'status_id' => $status->id,
|
||||
'company_id' => $company->id,
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$asset = Asset::where('asset_tag', $tag)->firstOrFail();
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Asset::class,
|
||||
'item_id' => $asset->id,
|
||||
'action_type' => 'create',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessory events
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function test_accessory_checkout_log_stores_the_accessorys_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$accessory = Accessory::factory()->create(['company_id' => $company->id]);
|
||||
$user = User::factory()->create();
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.accessories.checkout', $accessory), [
|
||||
'assigned_user' => $user->id,
|
||||
'checkout_to_type' => 'user',
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Accessory::class,
|
||||
'item_id' => $accessory->id,
|
||||
'action_type' => 'checkout',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_accessory_checkin_log_stores_the_accessorys_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$accessory = Accessory::factory()->checkedOutToUser()->create(['company_id' => $company->id]);
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$checkoutRecord = $accessory->checkouts->first();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.accessories.checkin', $checkoutRecord))
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Accessory::class,
|
||||
'item_id' => $accessory->id,
|
||||
'action_type' => 'checkin from',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Consumable events
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function test_consumable_checkout_log_stores_the_consumables_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$consumable = Consumable::factory()->create(['company_id' => $company->id]);
|
||||
$user = User::factory()->create();
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.consumables.checkout', $consumable), [
|
||||
'assigned_to' => $user->id,
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Consumable::class,
|
||||
'item_id' => $consumable->id,
|
||||
'action_type' => 'checkout',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Component events
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function test_component_checkout_log_stores_the_components_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$component = Component::factory()->create(['company_id' => $company->id]);
|
||||
$asset = Asset::factory()->create();
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.components.checkout', $component->id), [
|
||||
'assigned_to' => $asset->id,
|
||||
'assigned_qty' => 1,
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Component::class,
|
||||
'item_id' => $component->id,
|
||||
'action_type' => 'checkout',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_component_checkin_log_stores_the_components_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$component = Component::factory()->create(['company_id' => $company->id]);
|
||||
$asset = Asset::factory()->create();
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
// Check out first
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.components.checkout', $component->id), [
|
||||
'assigned_to' => $asset->id,
|
||||
'assigned_qty' => 1,
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$pivotId = $component->assets()->first()->pivot->id;
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.components.checkin', $pivotId), [
|
||||
'checkin_qty' => 1,
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Component::class,
|
||||
'item_id' => $component->id,
|
||||
'action_type' => 'checkin from',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// License events
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function test_license_checkout_log_stores_the_licenses_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$license = License::factory()->create(['company_id' => $company->id]);
|
||||
$seat = $license->freeSeats()->first();
|
||||
$user = User::factory()->create();
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->patchJson(route('api.licenses.seats.update', [$license->id, $seat->id]), [
|
||||
'assigned_to' => $user->id,
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
// The log is stored against the License (item_type), not the LicenseSeat
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => License::class,
|
||||
'item_id' => $license->id,
|
||||
'action_type' => 'checkout',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_license_checkin_log_stores_the_licenses_company_id(): void
|
||||
{
|
||||
$company = Company::factory()->create();
|
||||
$license = License::factory()->create(['company_id' => $company->id]);
|
||||
$seat = $license->freeSeats()->first();
|
||||
$user = User::factory()->create();
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
// Check out first
|
||||
$seat->assigned_to = $user->id;
|
||||
$seat->save();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->patchJson(route('api.licenses.seats.update', [$license->id, $seat->id]), [
|
||||
'assigned_to' => null,
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => License::class,
|
||||
'item_id' => $license->id,
|
||||
'action_type' => 'checkin from',
|
||||
'company_id' => $company->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Null company_id — items without a company should log null, not an error
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function test_asset_audit_log_company_id_is_null_when_asset_has_no_company(): void
|
||||
{
|
||||
$asset = Asset::factory()->create(['company_id' => null]);
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.asset.audit', $asset), ['note' => 'no company'])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Asset::class,
|
||||
'item_id' => $asset->id,
|
||||
'action_type' => 'audit',
|
||||
'company_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_asset_checkout_log_company_id_is_null_when_asset_has_no_company(): void
|
||||
{
|
||||
$asset = Asset::factory()->create(['company_id' => null]);
|
||||
$user = User::factory()->create();
|
||||
$admin = User::factory()->superuser()->create();
|
||||
|
||||
$this->actingAsForApi($admin)
|
||||
->postJson(route('api.asset.checkout', $asset), [
|
||||
'checkout_to_type' => 'user',
|
||||
'assigned_user' => $user->id,
|
||||
'status_id' => $asset->status_id,
|
||||
])
|
||||
->assertStatusMessageIs('success');
|
||||
|
||||
$this->assertDatabaseHas('action_logs', [
|
||||
'item_type' => Asset::class,
|
||||
'item_id' => $asset->id,
|
||||
'action_type' => 'checkout',
|
||||
'company_id' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,12 @@ use App\Events\CheckoutAccepted;
|
||||
use App\Models\Actionlog;
|
||||
use App\Models\Asset;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\CustomField;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Notifications\AcceptanceItemAcceptedNotification;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AssetAcceptanceTest extends TestCase
|
||||
@@ -204,6 +207,45 @@ class AssetAcceptanceTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_acceptance_email_includes_custom_fields_marked_show_in_email_and_not_encrypted(): void
|
||||
{
|
||||
Event::fake([CheckoutAccepted::class]);
|
||||
Notification::fake();
|
||||
$this->settings->enableAlertEmail();
|
||||
|
||||
$customField = CustomField::factory()->create([
|
||||
'name' => 'Cost Center',
|
||||
'show_in_email' => '1',
|
||||
'field_encrypted' => '0',
|
||||
])->fresh();
|
||||
|
||||
$asset = Asset::factory()->hasMultipleCustomFields([$customField])->create();
|
||||
$asset->{$customField->db_column} = 'ENG-42';
|
||||
$asset->save();
|
||||
|
||||
$checkoutAcceptance = CheckoutAcceptance::factory()
|
||||
->pending()
|
||||
->for($asset, 'checkoutable')
|
||||
->create();
|
||||
|
||||
$this->actingAs($checkoutAcceptance->assignedTo)
|
||||
->post(route('account.store-acceptance', $checkoutAcceptance), [
|
||||
'asset_acceptance' => 'accepted',
|
||||
])
|
||||
->assertRedirectToRoute('account.accept')
|
||||
->assertSessionHas('success');
|
||||
|
||||
Notification::assertSentTo(
|
||||
$checkoutAcceptance,
|
||||
function (AcceptanceItemAcceptedNotification $notification) {
|
||||
$rendered = $notification->toMail()->render();
|
||||
|
||||
return str_contains($rendered, 'Cost Center')
|
||||
&& str_contains($rendered, 'ENG-42');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function test_admin_can_complete_sign_in_place_acceptance_and_is_redirected_to_selected_destination()
|
||||
{
|
||||
Event::fake([CheckoutAccepted::class]);
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Mail\CheckinAssetMail;
|
||||
use App\Models\Accessory;
|
||||
use App\Models\Asset;
|
||||
use App\Models\Consumable;
|
||||
use App\Models\CustomField;
|
||||
use App\Models\LicenseSeat;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
@@ -99,6 +100,32 @@ class EmailNotificationsToUserUponCheckinTest extends TestCase
|
||||
Mail::assertNothingSent();
|
||||
}
|
||||
|
||||
public function test_checkin_email_includes_custom_fields_marked_show_in_email_and_not_encrypted()
|
||||
{
|
||||
$customField = CustomField::factory()->create([
|
||||
'name' => 'Cost Center',
|
||||
'show_in_email' => '1',
|
||||
'field_encrypted' => '0',
|
||||
])->fresh();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$asset = Asset::factory()->hasMultipleCustomFields([$customField])->assignedToUser($user)->create();
|
||||
$asset->{$customField->db_column} = 'ENG-42';
|
||||
$asset->save();
|
||||
|
||||
$asset->model->category->update(['checkin_email' => true]);
|
||||
|
||||
$this->fireCheckInEvent($asset, $user);
|
||||
|
||||
Mail::assertSent(CheckinAssetMail::class, function (CheckinAssetMail $mail) use ($user) {
|
||||
$rendered = $mail->render();
|
||||
|
||||
return $mail->hasTo($user->email)
|
||||
&& str_contains($rendered, 'Cost Center')
|
||||
&& str_contains($rendered, 'ENG-42');
|
||||
});
|
||||
}
|
||||
|
||||
private function fireCheckInEvent($asset, $user): void
|
||||
{
|
||||
event(new CheckoutableCheckedIn(
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\Asset;
|
||||
use App\Models\AssetModel;
|
||||
use App\Models\Category;
|
||||
use App\Models\CheckoutAcceptance;
|
||||
use App\Models\CustomField;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
@@ -106,6 +107,35 @@ class EmailNotificationsToUserUponCheckoutTest extends TestCase
|
||||
$this->assertUserSentEmail();
|
||||
}
|
||||
|
||||
public function test_email_includes_custom_fields_marked_show_in_email_and_not_encrypted()
|
||||
{
|
||||
$customField = CustomField::factory()->create([
|
||||
'name' => 'Cost Center',
|
||||
'show_in_email' => '1',
|
||||
'field_encrypted' => '0',
|
||||
])->fresh();
|
||||
|
||||
$asset = Asset::factory()->hasMultipleCustomFields([$customField])->create();
|
||||
$asset->{$customField->db_column} = 'ENG-42';
|
||||
$asset->save();
|
||||
|
||||
$this->category = $asset->model->category;
|
||||
$this->asset = $asset;
|
||||
$this->user = User::factory()->create();
|
||||
|
||||
$this->category->update(['checkin_email' => true]);
|
||||
|
||||
$this->fireCheckoutEvent();
|
||||
|
||||
Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) {
|
||||
$rendered = $mail->render();
|
||||
|
||||
return $mail->hasTo($this->user->email)
|
||||
&& str_contains($rendered, 'Cost Center')
|
||||
&& str_contains($rendered, 'ENG-42');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_handles_user_not_having_email_address_set()
|
||||
{
|
||||
$this->category->update(['checkin_email' => true]);
|
||||
|
||||
@@ -90,8 +90,10 @@ class ActivityReportTest extends TestCase
|
||||
|
||||
// I don't love this, since it doesn't test that we're actually storing the company ID appropriately
|
||||
// but it's better than what we had
|
||||
$response = $this->actingAsForApi($userInCompanyA)
|
||||
->getJson(route('api.activity.index'))
|
||||
$this->actingAsForApi($userInCompanyA)
|
||||
->getJson(route('api.activity.index', [
|
||||
'action_type' => 'update',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertJsonStructure([
|
||||
'rows',
|
||||
@@ -100,7 +102,9 @@ class ActivityReportTest extends TestCase
|
||||
|
||||
$this->actingAsForApi($userInCompanyB)
|
||||
->getJson(
|
||||
route('api.activity.index'))
|
||||
route('api.activity.index', [
|
||||
'action_type' => 'update',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertJsonStructure([
|
||||
'rows',
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\Department;
|
||||
use App\Models\User;
|
||||
use App\Notifications\WelcomeNotification;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Testing\Fluent\AssertableJson;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -64,7 +65,7 @@ class CreateUserTest extends TestCase
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$this->actingAsForApi(User::factory()->createUsers()->create())
|
||||
$response = $this->actingAsForApi(User::factory()->createUsers()->create())
|
||||
->postJson(route('api.users.store'), [
|
||||
'first_name' => 'Test First Name',
|
||||
'last_name' => 'Test Last Name',
|
||||
@@ -74,6 +75,7 @@ class CreateUserTest extends TestCase
|
||||
'activated' => '1',
|
||||
'email' => 'foo@example.org',
|
||||
'notes' => 'Test Note',
|
||||
'avatar' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEsAQMAAADXeXeBAAAABlBMVEX+AAD///+KQee0AAAACXBIWXMAAAsSAAALEgHS3X78AAAAB3RJTUUH5QQbCAoNcoiTQAAAACZJREFUaN7twTEBAAAAwqD1T20JT6AAAAAAAAAAAAAAAAAAAICnATvEAAEnf54JAAAAAElFTkSuQmCC',
|
||||
])
|
||||
->assertStatusMessageIs('success')
|
||||
->assertOk();
|
||||
@@ -85,10 +87,17 @@ class CreateUserTest extends TestCase
|
||||
'activated' => '1',
|
||||
'email' => 'foo@example.org',
|
||||
'notes' => 'Test Note',
|
||||
|
||||
]);
|
||||
|
||||
Notification::assertNothingSent();
|
||||
|
||||
$user = User::findOrFail($response['payload']['id']);
|
||||
|
||||
// assert against resized hash
|
||||
$this->assertEquals(
|
||||
'db2e13ba04318c99058ca429d67777322f48566b',
|
||||
sha1(Storage::disk('public')->get(app('users_upload_path').$user->avatar))
|
||||
);
|
||||
}
|
||||
|
||||
public function test_can_create_and_notify_user()
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\Group;
|
||||
use App\Models\Location;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class UpdateUserTest extends TestCase
|
||||
@@ -51,6 +52,7 @@ class UpdateUserTest extends TestCase
|
||||
'vip' => true,
|
||||
'start_date' => '2021-08-01',
|
||||
'end_date' => '2025-12-31',
|
||||
'avatar' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEsAQMAAADXeXeBAAAABlBMVEX+AAD///+KQee0AAAACXBIWXMAAAsSAAALEgHS3X78AAAAB3RJTUUH5QQbCAoNcoiTQAAAACZJREFUaN7twTEBAAAAwqD1T20JT6AAAAAAAAAAAAAAAAAAAICnATvEAAEnf54JAAAAAElFTkSuQmCC',
|
||||
])
|
||||
->assertOk()
|
||||
->assertStatus(200)
|
||||
@@ -80,6 +82,12 @@ class UpdateUserTest extends TestCase
|
||||
$this->assertEquals('2021-08-01', $user->start_date, 'Start date was not updated');
|
||||
$this->assertEquals('2025-12-31', $user->end_date, 'End date was not updated');
|
||||
|
||||
// assert against resized hash
|
||||
$this->assertEquals(
|
||||
'db2e13ba04318c99058ca429d67777322f48566b',
|
||||
sha1(Storage::disk('public')->get(app('users_upload_path').$user->avatar))
|
||||
);
|
||||
|
||||
// `groups` can be an id or array or ids
|
||||
$this->patch(route('api.users.update', $user), ['groups' => [$groupA->id, $groupB->id]]);
|
||||
|
||||
@@ -127,6 +135,7 @@ class UpdateUserTest extends TestCase
|
||||
'vip' => true,
|
||||
'start_date' => '2021-08-01',
|
||||
'end_date' => '2025-12-31',
|
||||
'avatar' => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEsAQMAAADXeXeBAAAABlBMVEX+AAD///+KQee0AAAACXBIWXMAAAsSAAALEgHS3X78AAAAB3RJTUUH5QQbCAoNcoiTQAAAACZJREFUaN7twTEBAAAAwqD1T20JT6AAAAAAAAAAAAAAAAAAAICnATvEAAEnf54JAAAAAElFTkSuQmCC',
|
||||
])
|
||||
->assertOk()
|
||||
->assertStatus(200)
|
||||
@@ -156,6 +165,12 @@ class UpdateUserTest extends TestCase
|
||||
$this->assertEquals('2021-08-01', $user->start_date, 'Start date was not updated');
|
||||
$this->assertEquals('2025-12-31', $user->end_date, 'End date was not updated');
|
||||
|
||||
// assert against resized hash
|
||||
$this->assertEquals(
|
||||
'db2e13ba04318c99058ca429d67777322f48566b',
|
||||
sha1(Storage::disk('public')->get(app('users_upload_path').$user->avatar))
|
||||
);
|
||||
|
||||
// `groups` can be an id or array or ids
|
||||
$this->patch(route('api.users.update', $user), ['groups' => [$groupA->id, $groupB->id]]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user