Compare commits

..

8 Commits

Author SHA1 Message Date
Brady Wetherington 251080d5b8 Make tests pass, at least to start with. Still needs cleanup 2024-06-06 17:12:26 +01:00
Brady Wetherington 3a3f26ba8b Clean up the fieldset experience for custom fields for users 2024-06-06 13:56:04 +01:00
Brady Wetherington 58656fbaec Clean up this migration so it runs forwards and backwards OK 2024-06-06 13:49:14 +01:00
Brady Wetherington 873d260f55 Refactor out common code for 'custom fields view' partial 2024-06-06 13:49:12 +01:00
Brady Wetherington 48fd6f3f26 Remove some code duplication 2024-06-06 13:47:34 +01:00
Brady Wetherington 05a68afab9 Wiring up custom fields for users - still some big UI challenges tho 2024-06-06 13:47:32 +01:00
Brady Wetherington 21ff0e748c Got a chunk of Custom Fields for Users worked out, still needs cleanup 2024-06-06 13:45:35 +01:00
Brady Wetherington d2b7828569 This is a squashed branch of all of the various commits that make up the new HasCustomFields trait.
This should allow us to add custom fields to just about anything we want to within Snipe-IT.

Below are the commits that have been squashed together:

Initial decoupling of custom field behavior from Assets for re-use

Add new DB columns to Custom Fields and fieldsets for 'type'

WIP: trying to figure out UI for custom fields for things other than Assets, find problematic places

Real progress towards getting to where this stuff might actually work...

Fix the table-name determining code for Custom Fields

Getting it closer to where Assets at least work

Rename the trait to it's new, even better name

Solid progress on the new Trait!

WIP: HasCustomFields, still working some stuff out

Got some basics working; creating custom fields and stuff

HasCustomFields now validates and saves

Starting to yank the other boilerplate code as things start to work (!)

Got the start of defaultValuesForCustomField() working

More progress (squash me!)

Add migrations for default_values_for_custom_fields table

WIP: more towards hasCustomFields trait

Progress cleaning up the PR, fixing FIXME's

New, passing HasCustomFieldsTrait test!

Fix date formatter helper for custom fields

Fixed more FIXME's
2024-06-06 13:35:38 +01:00
911 changed files with 7174 additions and 31663 deletions
+1 -11
View File
@@ -3115,8 +3115,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/23613427?v=4",
"profile": "https://github.com/bryanlopezinc",
"contributions": [
"code",
"test"
"code"
]
},
{
@@ -3127,15 +3126,6 @@
"contributions": [
"code"
]
},
{
"login": "Tyree",
"name": "Matt Tyree",
"avatar_url": "https://avatars.githubusercontent.com/u/5395363?v=4",
"profile": "https://github.com/Tyree",
"contributions": [
"doc"
]
}
]
}
-166
View File
@@ -1,166 +0,0 @@
# --------------------------------------------
# REQUIRED: DB SETUP
# --------------------------------------------
MYSQL_DATABASE=snipeit
MYSQL_USER=snipeit
MYSQL_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=develop
APP_DEBUG=false
# please regenerate the APP_KEY value by calling `docker-compose run --rm snipeit bash` and then `php artisan key:generate --show` and then copy paste the value here
APP_KEY=base64:3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ=
APP_URL=http://localhost:8000
APP_TIMEZONE='UTC'
APP_LOCALE=en
MAX_RESULTS=500
# --------------------------------------------
# REQUIRED: UPLOADED FILE STORAGE SETTINGS
# --------------------------------------------
PRIVATE_FILESYSTEM_DISK=local
PUBLIC_FILESYSTEM_DISK=local_public
# --------------------------------------------
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=mariadb
DB_DATABASE=snipeit
DB_USERNAME=snipeit
DB_PASSWORD=changeme1234
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS
# --------------------------------------------
DB_SSL=false
DB_SSL_IS_PAAS=false
DB_SSL_KEY_PATH=null
DB_SSL_CERT_PATH=null
DB_SSL_CA_PATH=null
DB_SSL_CIPHER=null
DB_SSL_VERIFY_SERVER=null
# --------------------------------------------
# REQUIRED: OUTGOING MAIL SERVER SETTINGS
# --------------------------------------------
MAIL_DRIVER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDR=you@example.com
MAIL_FROM_NAME='Snipe-IT'
MAIL_REPLYTO_ADDR=you@example.com
MAIL_REPLYTO_NAME='Snipe-IT'
MAIL_AUTO_EMBED_METHOD='attachment'
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: BACKUP SETTINGS
# --------------------------------------------
MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
SESSION_LIFETIME=12000
EXPIRE_ON_CLOSE=false
ENCRYPT=false
COOKIE_NAME=snipeit_session
COOKIE_DOMAIN=null
SECURE_COOKIES=false
API_TOKEN_EXPIRATION_YEARS=40
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=false
CORS_ALLOWED_ORIGINS=null
ENABLE_HSTS=false
# --------------------------------------------
# OPTIONAL: CACHE SETTINGS
# --------------------------------------------
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
CACHE_PREFIX=snipeit
# --------------------------------------------
# OPTIONAL: REDIS SETTINGS
# --------------------------------------------
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
# --------------------------------------------
# OPTIONAL: MEMCACHED SETTINGS
# --------------------------------------------
MEMCACHED_HOST=null
MEMCACHED_PORT=null
# --------------------------------------------
# OPTIONAL: PUBLIC S3 Settings
# --------------------------------------------
PUBLIC_AWS_SECRET_ACCESS_KEY=null
PUBLIC_AWS_ACCESS_KEY_ID=null
PUBLIC_AWS_DEFAULT_REGION=null
PUBLIC_AWS_BUCKET=null
PUBLIC_AWS_URL=null
PUBLIC_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: PRIVATE S3 Settings
# --------------------------------------------
PRIVATE_AWS_ACCESS_KEY_ID=null
PRIVATE_AWS_SECRET_ACCESS_KEY=null
PRIVATE_AWS_DEFAULT_REGION=null
PRIVATE_AWS_BUCKET=null
PRIVATE_AWS_URL=null
PRIVATE_AWS_BUCKET_ROOT=null
# --------------------------------------------
# OPTIONAL: AWS Settings
# --------------------------------------------
AWS_ACCESS_KEY_ID=null
AWS_SECRET_ACCESS_KEY=null
AWS_DEFAULT_REGION=null
# --------------------------------------------
# OPTIONAL: LOGIN THROTTLING
# --------------------------------------------
LOGIN_MAX_ATTEMPTS=5
LOGIN_LOCKOUT_DURATION=60
RESET_PASSWORD_LINK_EXPIRES=900
# --------------------------------------------
# OPTIONAL: MISC
# --------------------------------------------
LOG_CHANNEL=stderr
LOG_MAX_DAYS=10
APP_LOCKED=false
APP_CIPHER=AES-256-CBC
APP_FORCE_TLS=false
GOOGLE_MAPS_API=
LDAP_MEM_LIM=500M
LDAP_TIME_LIM=600
+12 -18
View File
@@ -1,18 +1,18 @@
# --------------------------------------------
# REQUIRED: DOCKER SPECIFIC SETTINGS
# REQUIRED: DB SETUP
# --------------------------------------------
APP_VERSION=v6.4.1
APP_PORT=8000
MYSQL_DATABASE=snipeit
MYSQL_USER=snipeit
MYSQL_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
# --------------------------------------------
# REQUIRED: BASIC APP SETTINGS
# --------------------------------------------
APP_ENV=production
APP_ENV=develop
APP_DEBUG=false
# Please regenerate the APP_KEY value by calling `docker compose run --rm snipeit php artisan key:generate --show`. Copy paste the value here
# please regenerate the APP_KEY value by calling `docker-compose run --rm snipeit bash` and then `php artisan key:generate --show` and then copy paste the value here
APP_KEY=base64:3ilviXqB9u6DX1NRcyWGJ+sjySF+H18CPDGb3+IVwMQ=
APP_URL=http://localhost:8000
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - TZ identifier
APP_TIMEZONE='UTC'
APP_LOCALE=en
MAX_RESULTS=500
@@ -27,12 +27,10 @@ PUBLIC_FILESYSTEM_DISK=local_public
# REQUIRED: DATABASE SETTINGS
# --------------------------------------------
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT='3306'
DB_HOST=mariadb
DB_DATABASE=snipeit
DB_USERNAME=snipeit
DB_PASSWORD=changeme1234
MYSQL_ROOT_PASSWORD=changeme1234
DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_CHARSET=utf8mb4
@@ -64,18 +62,13 @@ MAIL_REPLYTO_ADDR=you@example.com
MAIL_REPLYTO_NAME='Snipe-IT'
MAIL_AUTO_EMBED_METHOD='attachment'
# --------------------------------------------
# REQUIRED: DATA PROTECTION
# --------------------------------------------
ALLOW_BACKUP_DELETE=false
ALLOW_DATA_PURGE=false
# --------------------------------------------
# REQUIRED: IMAGE LIBRARY
# This should be gd or imagick
# --------------------------------------------
IMAGE_LIB=gd
# --------------------------------------------
# OPTIONAL: BACKUP SETTINGS
# --------------------------------------------
@@ -83,6 +76,7 @@ MAIL_BACKUP_NOTIFICATION_DRIVER=null
MAIL_BACKUP_NOTIFICATION_ADDRESS=null
BACKUP_ENV=true
# --------------------------------------------
# OPTIONAL: SESSION SETTINGS
# --------------------------------------------
@@ -97,7 +91,7 @@ API_TOKEN_EXPIRATION_YEARS=40
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1,172.0.0.0/8
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=false
@@ -115,7 +109,7 @@ CACHE_PREFIX=snipeit
# --------------------------------------------
# OPTIONAL: REDIS SETTINGS
# --------------------------------------------
REDIS_HOST=null
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
+1 -1
View File
@@ -73,7 +73,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.alpine
+1 -1
View File
@@ -73,7 +73,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push 'snipe-it' image
id: docker_build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
-1
View File
@@ -27,7 +27,6 @@ jobs:
php-version:
- "8.1"
- "8.2"
- "8.3"
name: PHP ${{ matrix.php-version }}
-75
View File
@@ -1,75 +0,0 @@
name: Tests in Postgres
on: workflow_dispatch
jobs:
tests:
runs-on: ubuntu-latest
services:
postgresql:
image: postgres
env:
POSTGRES_DB: snipeit
POSTGRES_USER: snipeit
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3
strategy:
fail-fast: false
matrix:
php-version:
- "8.2"
name: PHP ${{ matrix.php-version }}
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: "${{ matrix.php-version }}"
coverage: none
- uses: actions/checkout@v4
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Copy .env
run: |
cp -v .env.testing.example .env
cp -v .env.testing.example .env.testing
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Setup Laravel
env:
DB_CONNECTION: pgsql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
DB_USERNAME: snipeit
DB_PASSWORD: password
run: |
php artisan key:generate
php artisan migrate --force
php artisan passport:install
chmod -R 777 storage bootstrap/cache
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: pgsql
DB_DATABASE: snipeit
DB_PORT: ${{ job.services.postgresql.ports[5432] }}
DB_USERNAME: snipeit
DB_PASSWORD: password
run: php artisan test --parallel
+2 -2
View File
@@ -4,7 +4,7 @@
"DOC3": "Please don't rely on these versions for planning upgrades unless you've fetched the most recent version",
"DOC4": "You should really just ignore it and run upgrade.php. Really",
"php_min_version": "8.1.0",
"php_max_major_minor": "8.3",
"php_max_wontwork": "8.4.0",
"php_max_major_minor": "8.2",
"php_max_wontwork": "8.3.0",
"current_snipeit_version": "7.0"
}
+1 -2
View File
@@ -444,11 +444,10 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
<td align="center" valign="top" width="14.28%"><a href="https://www.corelight.com/"><img src="https://avatars.githubusercontent.com/u/1496725?v=4?s=110" width="110px;" alt="i_virus"/><br /><sub><b>i_virus</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=chandanchowdhury" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gitgrimbo"><img src="https://avatars.githubusercontent.com/u/1020541?v=4?s=110" width="110px;" alt="Paul Grime"/><br /><sub><b>Paul Grime</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=gitgrimbo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://leeporte.co.uk"><img src="https://avatars.githubusercontent.com/u/922815?v=4?s=110" width="110px;" alt="Lee Porte"/><br /><sub><b>Lee Porte</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=LeePorte" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bryanlopezinc"><img src="https://avatars.githubusercontent.com/u/23613427?v=4?s=110" width="110px;" alt="BRYAN "/><br /><sub><b>BRYAN </b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bryanlopezinc" title="Code">💻</a> <a href="https://github.com/snipe/snipe-it/commits?author=bryanlopezinc" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bryanlopezinc"><img src="https://avatars.githubusercontent.com/u/23613427?v=4?s=110" width="110px;" alt="BRYAN "/><br /><sub><b>BRYAN </b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=bryanlopezinc" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/U-H-T"><img src="https://avatars.githubusercontent.com/u/64061710?v=4?s=110" width="110px;" alt="U-H-T"/><br /><sub><b>U-H-T</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=U-H-T" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tyree"><img src="https://avatars.githubusercontent.com/u/5395363?v=4?s=110" width="110px;" alt="Matt Tyree"/><br /><sub><b>Matt Tyree</b></sub></a><br /><a href="https://github.com/snipe/snipe-it/commits?author=Tyree" title="Documentation">📖</a></td>
</tr>
</tbody>
</table>
+1 -18
View File
@@ -2,7 +2,6 @@
namespace App\Console\Commands;
use App\Events\UserMerged;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Console\Command;
@@ -52,7 +51,7 @@ class MergeUsersByUsername extends Command
$bad_users = User::where('username', '=', trim($parts[0]))
->whereNull('deleted_at')
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations','uploads', 'acceptances')
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations')
->get();
@@ -106,26 +105,10 @@ class MergeUsersByUsername extends Command
$managedLocation->save();
}
foreach ($bad_user->uploads as $upload) {
$this->info('Updating upload log record '.$upload->id.' to user '.$user->id);
$upload->item_id = $user->id;
$upload->save();
}
foreach ($bad_user->acceptances as $acceptance) {
$this->info('Updating acceptance log record '.$acceptance->id.' to user '.$user->id);
$acceptance->item_id = $user->id;
$acceptance->save();
}
// Mark the user as deleted
$this->info('Marking the user as deleted');
$bad_user->deleted_at = Carbon::now()->timestamp;
$bad_user->save();
event(new UserMerged($bad_user, $user, null));
}
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ class UserMerged
*
* @return void
*/
public function __construct(User $from_user, User $to_user, ?User $admin)
public function __construct(User $from_user, User $to_user, User $admin)
{
$this->merged_from = $from_user;
$this->merged_to = $to_user;
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\Gate;
/*********************
* These two helper methods are more designed for being re-used with the new HasCustomFields Trait
*
* The 'transform' method is designed for BlahTransformer things that need to return custom field values.
*
* The 'present' method is designed for when you're trying to generate fieldlists for use in Bootstrap tables
* - typically the 'dataTableLayout' method
*
*********************/
class CustomFieldHelper {
static function transform($fieldset, $item) {
if ($fieldset && ($fieldset->fields->count() > 0)) {
$fields_array = [];
foreach ($fieldset->fields as $field) {
if ($field->isFieldDecryptable($item->{$field->db_column})) {
$decrypted = Helper::gracefulDecrypt($field, $item->{$field->db_column});
$value = (Gate::allows('assets.view.encrypted_custom_fields')) ? $decrypted : strtoupper(trans('admin/custom_fields/general.encrypted'));
if ($field->format == 'DATE'){
if (Gate::allows('assets.view.encrypted_custom_fields')){
$value = Helper::getFormattedDateObject($value, 'date', false);
} else {
$value = strtoupper(trans('admin/custom_fields/general.encrypted'));
}
}
$fields_array[$field->name] = [
'field' => e($field->db_column),
'value' => e($value),
'field_format' => $field->format,
'element' => $field->element,
];
} else {
$value = $item->{$field->db_column};
if (($field->format == 'DATE') && (!is_null($value)) && ($value!='')){
$value = Helper::getFormattedDateObject($value, 'date', false);
}
$fields_array[$field->name] = [
'field' => e($field->db_column),
'value' => e($value),
'field_format' => $field->format,
'element' => $field->element,
];
}
return $fields_array;
}
} else {
return new \stdClass; // HACK to force generation of empty object instead of empty list
}
}
static function present($field) {
return [
'field' => 'custom_fields.'.$field->db_column,
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => $field->name,
'formatter'=> 'customFieldsFormatter',
'escape' => true,
'class' => ($field->field_encrypted == '1') ? 'css-padlock' : '',
'visible' => ($field->show_in_listview == '1') ? true : false,
];
}
}
+11
View File
@@ -651,6 +651,17 @@ class Helper
return $customfields;
}
/**
* Get all of the different types of custom fields there are
* TODO - how to make this more general? Or more useful? or more dynamic?
* idea - key of classname, *value* of trans? (thus having to make this a method, which is fine)
*/
static $itemtypes_having_custom_fields = [
0 => \App\Models\Asset::class,
1 => \App\Models\User::class,
// 2 => \App\Models\Accessory::class
];
/**
* Get the list of custom field formats in an array to make a dropdown menu
*
+14 -16
View File
@@ -17,24 +17,22 @@ class ActionlogController extends Controller
$disk = config('filesystems.default');
switch (config("filesystems.disks.$disk.driver")) {
case 's3':
$file = 'private_uploads/signatures/'.$filename;
return redirect()->away(Storage::disk($disk)->temporaryUrl($file, now()->addMinutes(5)));
default:
$this->authorize('view', \App\Models\Asset::class);
$file = config('app.private_uploads').'/signatures/'.$filename;
$filetype = Helper::checkUploadIsImage($file);
case 's3':
$file = 'private_uploads/signatures/'.$filename;
return redirect()->away(Storage::disk($disk)->temporaryUrl($file, now()->addMinutes(5)));
default:
$this->authorize('view', \App\Models\Asset::class);
$file = config('app.private_uploads').'/signatures/'.$filename;
$filetype = Helper::checkUploadIsImage($file);
$contents = file_get_contents($file, false, stream_context_create(['http' => ['ignore_errors' => true]]));
if ($contents === false) {
Log::warning('File '.$file.' not found');
return false;
} else {
return Response::make($contents)->header('Content-Type', $filetype);
}
}
$contents = file_get_contents($file, false, stream_context_create(['http' => ['ignore_errors' => true]]));
if ($contents === false) {
Log::warning('File '.$file.' not found');
return false;
} else {
return Response::make($contents)->header('Content-Type', $filetype);
}
}
public function getStoredEula($filename){
$this->authorize('view', \App\Models\Asset::class);
$file = config('app.private_uploads').'/eula-pdfs/'.$filename;
@@ -1,219 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use Illuminate\Support\Facades\Storage;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Gate;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Actionlog;
use \Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use DB;
use Illuminate\Http\Request;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Support\Facades\Log;
use Input;
use Paginator;
use Slack;
use Str;
use TCPDF;
use Validator;
use Route;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function store(UploadFileRequest $request, $assetId = null)
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $asset);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assets')) {
Storage::makeDirectory('private_uploads/assets', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
$asset->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.upload.success')));
}
// We only reach here if no files were included in the POST, so tell the user this
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function list($assetId = null)
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($asset->id)) {
$this->authorize('view', $asset);
// Check that there are some uploads on this asset that can be listed
if ($asset->uploads->count() > 0) {
$files = array();
foreach ($asset->uploads as $upload) {
array_push($files, $upload);
}
// Give the list of files back to the user
return response()->json(Helper::formatStandardApiResponse('success', $files, trans('admin/hardware/message.upload.success')));
}
// There are no files.
return response()->json(Helper::formatStandardApiResponse('success', array(), trans('admin/hardware/message.upload.success')));
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error')), 500);
}
/**
* Check for permissions and display the file.
*
* @param int $assetId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function show($assetId = null, $fileId = null)
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($asset->id)) {
$this->authorize('view', $asset);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assets/'.$log->filename;
\Log::debug('Checking for '.$file);
if ($log->action_type == 'audit') {
$file = 'private_uploads/audits/'.$log->filename;
}
// Check the file actually exists on the filesystem
if (! Storage::exists($file)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.does_not_exist', ['id' => $fileId])), 404);
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download($file, $log->filename, $headers);
}
return StorageHelper::downloader($file);
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function destroy($assetId = null, $fileId = null)
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 404);
}
$rel_path = 'private_uploads/assets';
// the asset is valid
if (isset($asset->id)) {
$this->authorize('update', $asset);
// Check for the file
$log = Actionlog::find($fileId);
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists($rel_path.'/'.$log->filename)) {
Storage::delete($rel_path.'/'.$log->filename);
}
// Delete the record of the file
$log->delete();
// All deleting done - notify the user of success
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/hardware/message.deletefile.success')), 200);
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
}
@@ -67,7 +67,7 @@ class AssetModelsController extends Controller
'models.deleted_at',
'models.updated_at',
])
->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues')
->with('category', 'depreciation', 'manufacturer')
->withCount('assets as assets_count');
if ($request->input('status')=='deleted') {
@@ -120,7 +120,7 @@ class AssetsController extends Controller
$filter = json_decode($request->input('filter'), true);
}
$all_custom_fields = CustomField::all(); //used as a 'cache' of custom fields throughout this page load
$all_custom_fields = CustomField::where('type', Asset::class); //used as a 'cache' of custom fields throughout this page load
foreach ($all_custom_fields as $field) {
$allowed_columns[] = $field->db_column_name();
}
@@ -585,7 +585,8 @@ class AssetsController extends Controller
}
$asset = $request->handleImages($asset);
$asset = $asset->handleCustomFieldsForStoring($request);
$asset->customFill($request, Auth::user(), true);
if ($asset->save()) {
@@ -647,8 +648,8 @@ class AssetsController extends Controller
}
$asset = $request->handleImages($asset);
$asset = $asset->handleCustomFieldsForStoring($request);
$problems_updating_encrypted_custom_fields = !$asset->customFill($request, Auth::user());
if ($asset->save()) {
if (($request->filled('assigned_user')) && ($target = User::find($request->get('assigned_user')))) {
@@ -35,7 +35,7 @@ class CustomFieldsetsController extends Controller
public function index()
{
$this->authorize('index', CustomField::class);
$fieldsets = CustomFieldset::withCount('fields as fields_count', 'models as models_count')->get();
$fieldsets = CustomFieldset::withCount('fields as fields_count')->get();
return (new CustomFieldsetsTransformer)->transformCustomFieldsets($fieldsets, $fieldsets->count());
}
@@ -125,7 +125,7 @@ class CustomFieldsetsController extends Controller
$this->authorize('delete', CustomField::class);
$fieldset = CustomFieldset::findOrFail($id);
$modelsCount = $fieldset->models->count();
$modelsCount = $fieldset->customizables()->count();
$fieldsCount = $fieldset->fields->count();
if (($modelsCount > 0) || ($fieldsCount > 0)) {
@@ -244,7 +244,7 @@ class LocationsController extends Controller
if (! $location->isDeletable()) {
return response()
->json(Helper::formatStandardApiResponse('error', null, trans('admin/locations/message.assoc_users')));
->json(Helper::formatStandardApiResponse('error', null, trans('admin/companies/message.assoc_users')));
}
$this->authorize('delete', $location);
$location->delete();
@@ -78,14 +78,13 @@ class ReportsController extends Controller
];
$total = $actionlogs->count();
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
$offset = ($request->input('offset') > $actionlogs->count()) ? $actionlogs->count() : app('api_offset_value');
$limit = app('api_limit_value');
$sort = in_array($request->input('sort'), $allowed_columns) ? e($request->input('sort')) : 'created_at';
$order = ($request->input('order') == 'asc') ? 'asc' : 'desc';
$total = $actionlogs->count();
$actionlogs = $actionlogs->orderBy($sort, $order)->skip($offset)->take($limit)->get();
+171 -136
View File
@@ -13,8 +13,8 @@ use App\Http\Transformers\SelectlistTransformer;
use App\Http\Transformers\UsersTransformer;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Accessory;
use App\Models\Consumable;
use App\Models\Company;
use App\Models\CustomField;
use App\Models\License;
use App\Models\User;
use App\Notifications\CurrentInventory;
@@ -23,7 +23,6 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
use App\Http\Requests\DeleteUserRequest;
class UsersController extends Controller
{
@@ -33,13 +32,13 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
*
* @return array
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$this->authorize('view', User::class);
$users = User::select([
$allowed_columns = [
'users.activated',
'users.created_by',
'users.address',
@@ -77,7 +76,12 @@ class UsersController extends Controller
'users.autoassign_licenses',
'users.website',
])->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy', 'managesUsers', 'managedLocations')
];
foreach (CustomField::where('type', User::class)->get() as $field) {
$allowed_columns[] = $field->db_column_name();
}
$users = User::select($allowed_columns)->with('manager', 'groups', 'userloc', 'company', 'department', 'assets', 'licenses', 'accessories', 'consumables', 'createdBy',)
->withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'managesUsers as manages_users_count', 'managedLocations as manages_locations_count');
@@ -205,6 +209,9 @@ class UsersController extends Controller
$users->where('autoassign_licenses', '=', $request->input('autoassign_licenses'));
}
if ($request->filled('location_id') != '') {
$users = $users->UserLocation($request->input('location_id'), $request->input('search'));
}
if (($request->filled('deleted')) && ($request->input('deleted') == 'true')) {
$users = $users->onlyTrashed();
@@ -247,6 +254,10 @@ class UsersController extends Controller
'jobtitle',
'username',
'employee_num',
'assets',
'accessories',
'consumables',
'licenses',
'groups',
'activated',
'created_at',
@@ -284,6 +295,10 @@ class UsersController extends Controller
// Apply companyable scope
$users = Company::scopeCompanyables($users);
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $users->count()) ? $users->count() : app('api_offset_value');
$limit = app('api_limit_value');
@@ -316,6 +331,8 @@ class UsersController extends Controller
]
)->where('show_in_list', '=', '1');
$users = Company::scopeCompanyables($users);
if ($request->filled('search')) {
$users = $users->where(function ($query) use ($request) {
$query->SimpleNameSearch($request->get('search'))
@@ -357,7 +374,7 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @param \Illuminate\Http\Request $request
* @return array | \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function store(SaveUserRequest $request)
{
@@ -385,7 +402,9 @@ class UsersController extends Controller
}
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
$user->customFill($request,Auth::user());
if ($user->save()) {
if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups'));
@@ -404,13 +423,15 @@ class UsersController extends Controller
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @param int $id
* @return array | \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function show($id)
{
$this->authorize('view', User::class);
if ($user = User::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'managesUsers as manages_users_count', 'managedLocations as manages_locations_count')->find($id)) {
$user = User::withCount('assets as assets_count', 'licenses as licenses_count', 'accessories as accessories_count', 'consumables as consumables_count', 'managesUsers as manages_users_count', 'managedLocations as manages_locations_count');
if ($user = Company::scopeCompanyables($user)->find($id)) {
$this->authorize('view', $user);
return (new UsersTransformer)->transformUser($user);
}
@@ -427,89 +448,88 @@ class UsersController extends Controller
* @since [v4.0]
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function update(SaveUserRequest $request, $id)
{
$this->authorize('update', User::class);
if ($user = User::find($id)) {
$user = User::findOrFail($id);
$user = Company::scopeCompanyables($user)->find($id);
$this->authorize('update', $user);
/**
* This is a janky hack to prevent people from changing admin demo user data on the public demo.
*
* The $ids 1 and 2 are special since they are seeded as superadmins in the demo seeder.
*
* Thanks, jerks. You are why we can't have nice things. - snipe
*
*/
$this->authorize('update', $user);
/**
* This is a janky hack to prevent people from changing admin demo user data on the public demo.
* The $ids 1 and 2 are special since they are seeded as superadmins in the demo seeder.
* Thanks, jerks. You are why we can't have nice things. - snipe
*
*/
if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Permission denied. You cannot update user information via API on the demo.'));
}
$user->fill($request->all());
if ($user->id == $request->input('manager_id')) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
}
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
// We need to use has() instead of filled()
// here because we need to overwrite permissions
// if someone needs to null them out
if ($request->has('permissions')) {
$permissions_array = $request->input('permissions');
// Strip out the individual superuser permission if the API user isn't a superadmin
if (!Auth::user()->isSuperUser()) {
unset($permissions_array['superuser']);
}
$user->permissions = $permissions_array;
}
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
if ($user->save()) {
// Check if the request has groups passed and has a value, AND that the user us a superuser
if (($request->has('groups')) && (Auth::user()->isSuperUser())) {
$validator = Validator::make($request->only('groups'), [
'groups.*' => 'integer|exists:permission_groups,id',
]);
if ($validator->fails()) {
return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()));
}
// Sync the groups since the user is a superuser and the groups pass validation
$user->groups()->sync($request->input('groups'));
}
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Permission denied. You cannot update user information via API on the demo.'));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
$user->fill($request->all());
if ($user->id == $request->input('manager_id')) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
}
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
// We need to use has() instead of filled()
// here because we need to overwrite permissions
// if someone needs to null them out
if ($request->has('permissions')) {
$permissions_array = $request->input('permissions');
// Strip out the individual superuser permission if the API user isn't a superadmin
if (! Auth::user()->isSuperUser()) {
unset($permissions_array['superuser']);
}
$user->permissions = $permissions_array;
}
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)->update(['location_id' => $request->input('location_id', null)]);
app('App\Http\Requests\ImageUploadRequest')->handleImages($user, 600, 'image', 'avatars', 'avatar');
$user->customFill($request,Auth::user());
if ($user->save()) {
// Check if the request has groups passed and has a value, AND that the user us a superuser
if (($request->has('groups')) && (Auth::user()->isSuperUser())) {
$validator = Validator::make($request->only('groups'), [
'groups.*' => 'integer|exists:permission_groups,id',
]);
if ($validator->fails()) {
return response()->json(Helper::formatStandardApiResponse('error', null, $validator->errors()));
}
// Sync the groups since the user is a superuser and the groups pass validation
$user->groups()->sync($request->input('groups'));
}
return response()->json(Helper::formatStandardApiResponse('success', (new UsersTransformer)->transformUser($user), trans('admin/users/message.success.update')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
}
/**
@@ -518,15 +538,41 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.0]
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @return \Illuminate\Http\Response
*/
public function destroy(DeleteUserRequest $request, $id)
public function destroy($id)
{
$this->authorize('delete', User::class);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($id);
$this->authorize('delete', $user);
if ($user = User::withTrashed()->find($id)) {
if ($user) {
$this->authorize('delete', $user);
if ($user->id === Auth::id()) {
// Redirect to the user management page
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.cannot_delete_yourself')));
}
if (($user->assets) && ($user->assets->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('admin/users/message.error.delete_has_assets_var', $user->assets()->count(), ['count'=> $user->assets()->count()])));
}
if (($user->licenses) && ($user->licenses->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('admin/users/message.error.delete_has_licenses_var', $user->licenses()->count(), ['count'=> $user->licenses()->count()])));
}
if (($user->accessories) && ($user->accessories->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('admin/users/message.error.delete_has_accessories_var', $user->accessories()->count(), ['count'=> $user->accessories()->count()])));
}
if (($user->managedLocations()) && ($user->managedLocations()->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('admin/users/message.error.delete_has_locations_var', $user->managedLocations()->count(), ['count'=> $user->managedLocations()->count()])));
}
if (($user->managesUsers()) && ($user->managesUsers()->count() > 0)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('admin/users/message.error.delete_has_users_var', $user->managesUsers()->count(), ['count'=> $user->managesUsers()->count()])));
}
if ($user->delete()) {
@@ -541,13 +587,9 @@ class UsersController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.delete')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')));
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.error.delete')));
}
/**
@@ -556,42 +598,39 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @param $userId
* @return array | \Illuminate\Http\JsonResponse
* @return string JSON
*/
public function assets(Request $request, $id)
{
$this->authorize('view', User::class);
$this->authorize('view', Asset::class);
if ($user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id)) {
$this->authorize('view', $user);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($id);
$this->authorize('view', $user);
$assets = Asset::where('assigned_to', '=', $id)->where('assigned_type', '=', User::class)->with('model');
$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);
// Filter on category ID
if ($request->filled('category_id')) {
$assets = $assets->InCategory($request->input('category_id'));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('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);
}
/**
@@ -606,21 +645,17 @@ class UsersController extends Controller
public function emailAssetList(Request $request, $id)
{
$this->authorize('update', User::class);
$user = User::findOrFail($id);
$user = Company::scopeCompanyables($user)->find($id);
$this->authorize('update', $user);
if ($user = User::find($id)) {
$this->authorize('update', $user);
if (empty($user->email)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.inventorynotification.error')));
}
$user->notify((new CurrentInventory($user)));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.inventorynotification.success')));
if (empty($user->email)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.inventorynotification.error')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
$user->notify((new CurrentInventory($user)));
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.inventorynotification.success')));
}
/**
@@ -629,14 +664,14 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @param $userId
* @return array | \Illuminate\Http\JsonResponse
* @return string JSON
*/
public function consumables(Request $request, $id)
{
$this->authorize('view', User::class);
$this->authorize('view', Consumable::class);
$user = User::findOrFail($id);
$this->authorize('view', $user);
$this->authorize('update', $user);
$consumables = $user->consumables;
return (new ConsumablesTransformer)->transformConsumables($consumables, $consumables->count(), $request);
}
@@ -647,7 +682,7 @@ class UsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v4.6.14]
* @param $userId
* @return array
* @return string JSON
*/
public function accessories($id)
{
@@ -666,7 +701,7 @@ class UsersController extends Controller
* @author [N. Mathar] [<snipe@snipe.net>]
* @since [v5.0]
* @param $userId
* @return array | \Illuminate\Http\JsonResponse
* @return string JSON
*/
public function licenses($id)
{
@@ -729,7 +764,7 @@ class UsersController extends Controller
* @author [Juan Font] [<juanfontalonso@gmail.com>]
* @since [v4.4.2]
* @param \Illuminate\Http\Request $request
* @return array
* @return \Illuminate\Http\Response
*/
public function getCurrentUserInfo(Request $request)
{
@@ -742,14 +777,12 @@ class UsersController extends Controller
* @author [E. Taylor] [<dev@evantaylor.name>]
* @param int $userId
* @since [v6.0.0]
* @return \Illuminate\Http\JsonResponse
* @return JsonResponse
*/
public function restore($userId)
public function restore($userId = null)
{
$this->authorize('delete', User::class);
if ($user = User::withTrashed()->find($userId)) {
$this->authorize('delete', $user);
if ($user->deleted_at == '') {
@@ -768,6 +801,8 @@ class UsersController extends Controller
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/users/message.success.restored')), 200);
}
// Check validation to make sure we're not restoring a user with the same username as an existing user
return response()->json(Helper::formatStandardApiResponse('error', null, $user->getErrors()));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found')), 200);
+8 -17
View File
@@ -9,6 +9,7 @@ use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CustomField;
use App\Models\User;
use App\Models\DefaultValuesForCustomFields;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\View;
@@ -85,7 +86,7 @@ class AssetModelsController extends Controller
$model->category_id = $request->input('category_id');
$model->notes = $request->input('notes');
$model->user_id = Auth::id();
$model->requestable = $request->has('requestable');
$model->requestable = Request::has('requestable');
if ($request->input('fieldset_id') != '') {
$model->fieldset_id = $request->input('fieldset_id');
@@ -163,7 +164,7 @@ class AssetModelsController extends Controller
$model->notes = $request->input('notes');
$model->requestable = $request->input('requestable', '0');
$this->removeCustomFieldsDefaultValues($model);
DefaultValuesForCustomFields::forPivot($model, Asset::class)->delete();
$model->fieldset_id = $request->input('fieldset_id');
@@ -288,7 +289,7 @@ class AssetModelsController extends Controller
public function show($modelId = null)
{
$this->authorize('view', AssetModel::class);
$model = AssetModel::withTrashed()->find($modelId);
$model = AssetModel::withTrashed()->withCount('assets')->find($modelId);
if (isset($model->id)) {
return view('models/view', compact('model'));
@@ -480,7 +481,7 @@ class AssetModelsController extends Controller
}
/**
* Adds default values to a model (as long as they are truthy)
* Adds default values to a model (as long as they are truthy) (does this mean I cannot set a default value of 0?)
*
* @param AssetModel $model
* @param array $defaultValues
@@ -515,22 +516,12 @@ class AssetModelsController extends Controller
}
foreach ($defaultValues as $customFieldId => $defaultValue) {
if(is_array($defaultValue)){
$model->defaultValues()->attach($customFieldId, ['default_value' => implode(', ', $defaultValue)]);
}elseif ($defaultValue) {
$model->defaultValues()->attach($customFieldId, ['default_value' => $defaultValue]);
if (is_array($defaultValue)) {
$defaultValue = implode(', ', $defaultValue);
}
DefaultValuesForCustomFields::updateOrCreate(['custom_field_id' => $customFieldId, 'item_pivot_id' => $model->id], ['default_value' => $defaultValue]);
}
return true;
}
/**
* Removes all default values
*
* @return void
*/
private function removeCustomFieldsDefaultValues(AssetModel $model)
{
$model->defaultValues()->detach();
}
}
@@ -160,7 +160,7 @@ class AssetsController extends Controller
$asset = $request->handleImages($asset);
}
$asset = $asset->handleCustomFieldsForStoring($request);
$asset->customFill($request, Auth::user()); // Update custom fields in the database.
// Validate the asset before saving
if ($asset->isValid() && $asset->save()) {
@@ -348,7 +348,32 @@ class AssetsController extends Controller
$asset->notes = $request->input('notes');
$asset = $request->handleImages($asset);
$asset = $asset->handleCustomFieldsForStoring($request);
// Update custom fields in the database.
// Validation for these fields is handlded through the AssetRequest form request
// FIXME: No idea why this is returning a Builder error on db_column_name.
// Need to investigate and fix. Using static method for now.
$model = AssetModel::find($request->get('model_id'));
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$asset->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$asset->{$field->db_column} = $request->input($field->db_column);
}
}
}
}
if ($asset->save()) {
return redirect()->route('hardware.show', $assetId)
@@ -858,7 +883,7 @@ class AssetsController extends Controller
if ($request->input('update_location') == '1') {
$asset->location_id = $request->input('location_id');
}
/**
* Invoke Watson Validating to check the asset itself and check to make sure it saved correctly.
@@ -427,6 +427,10 @@ class LoginController extends Controller
return redirect()->route('two-factor')->with('error', trans('auth/message.two_factor.code_required'));
}
if (! $request->has('two_factor_secret')) { // TODO this seems almost the same as above?
return redirect()->route('two-factor')->with('error', 'Two-factor code is required.');
}
$user = Auth::user();
$secret = $request->input('two_factor_secret');
@@ -435,7 +439,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()->route('home')->with('success', 'You are logged in!');
}
return redirect()->route('two-factor')->with('error', trans('auth/message.two_factor.invalid_code'));
@@ -533,7 +537,7 @@ class LoginController extends Controller
$minutes = round($seconds / 60);
$message = trans('auth/message.throttle', ['minutes' => $minutes]);
$message = \Lang::get('auth/message.throttle', ['minutes' => $minutes]);
return redirect()->back()
->withInput($request->only($this->username(), 'remember'))
+26 -10
View File
@@ -6,9 +6,11 @@ use App\Helpers\Helper;
use App\Http\Requests\CustomFieldRequest;
use App\Models\CustomField;
use App\Models\CustomFieldset;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
/**
* This controller handles all actions related to Custom Asset Fields for
* the Snipe-IT Asset Management application.
@@ -20,6 +22,7 @@ use Illuminate\Http\Request;
*/
class CustomFieldsController extends Controller
{
/**
* Returns a view with a listing of custom fields.
*
@@ -28,12 +31,16 @@ class CustomFieldsController extends Controller
* @return \Illuminate\Support\Facades\View
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function index()
public function index(Request $request)
{
$this->authorize('view', CustomField::class);
if ($request->input('tab') == 1) {
// Users section, make sure to auto-create the first fieldset if so
CustomFieldset::firstOrCreate(['type' => Helper::$itemtypes_having_custom_fields[1]], ['name' => 'default']);
}
$fieldsets = CustomFieldset::with('fields', 'models')->get();
$fields = CustomField::with('fieldset')->get();
$fieldsets = CustomFieldset::with('fields')->where("type", Helper::$itemtypes_having_custom_fields[$request->get('tab', 0)])->get(); //cannot eager-load 'customizable' because it's not a relation
$fields = CustomField::with('fieldset')->where("type", Helper::$itemtypes_having_custom_fields[$request->get('tab', 0)])->get();
return view('custom_fields.index')->with('custom_fieldsets', $fieldsets)->with('custom_fields', $fields);
}
@@ -66,7 +73,7 @@ class CustomFieldsController extends Controller
public function create(Request $request)
{
$this->authorize('create', CustomField::class);
$fieldsets = CustomFieldset::get();
$fieldsets = CustomFieldset::where('type', Helper::$itemtypes_having_custom_fields[$request->get('tab')])->get();
return view('custom_fields.fields.edit', [
'predefinedFormats' => Helper::predefined_formats(),
@@ -110,8 +117,10 @@ class CustomFieldsController extends Controller
"auto_add_to_fieldsets" => $request->get("auto_add_to_fieldsets", 0),
"show_in_listview" => $request->get("show_in_listview", 0),
"show_in_requestable_list" => $request->get("show_in_requestable_list", 0),
"user_id" => Auth::id()
"user_id" => Auth::id(),
]);
// not mass-assignable; must be manual
$field->type = Helper::$itemtypes_having_custom_fields[$request->get('tab')];
if ($request->filled('custom_format')) {
@@ -124,14 +133,17 @@ class CustomFieldsController extends Controller
// Sync fields with fieldsets
$fieldset_array = $request->input('associate_fieldsets');
if ($request->has('associate_fieldsets') && (is_array($fieldset_array))) {
if ($request->get('tab') == 1) {
$fieldset_array = [CustomFieldset::firstOrCreate(['type' => User::class], ['name' => 'default'])->id => true];
}
if (($request->has('associate_fieldsets') || $request->get('tab') == 1) && (is_array($fieldset_array))) {
$field->fieldset()->sync(array_keys($fieldset_array));
} else {
$field->fieldset()->sync([]);
}
return redirect()->route('fields.index')->with('success', trans('admin/custom_fields/message.field.create.success'));
return redirect()->route('fields.index', ['tab' => $request->get('tab', 0)])->with('success', trans('admin/custom_fields/message.field.create.success'));
}
return redirect()->back()->with('selected_fieldsets', $request->input('associate_fieldsets'))->withInput()
@@ -184,12 +196,16 @@ class CustomFieldsController extends Controller
if ($field = CustomField::find($field_id)) {
$this->authorize('delete', $field);
if ($field->type == User::class) {
$field->fieldset()->detach(); // remove from 'default' group (and others, if they exist in the future!)
}
if (($field->fieldset) && ($field->fieldset->count() > 0)) {
return redirect()->back()->withErrors(['message' => 'Field is in-use']);
}
$type = $field->type;
$field->delete();
return redirect()->route("fields.index")
->with("success", trans('admin/custom_fields/message.field.delete.success'));
return redirect()->route('fields.index', ['tab' => array_search($type, Helper::$itemtypes_having_custom_fields)])
->with('success', trans('admin/custom_fields/message.field.delete.success'));
}
return redirect()->back()->withErrors(['message' => 'Field does not exist']);
@@ -290,7 +306,7 @@ class CustomFieldsController extends Controller
$field->fieldset()->sync([]);
}
return redirect()->route('fields.index')->with('success', trans('admin/custom_fields/message.field.update.success'));
return redirect()->route('fields.index', ['tab' => $request->get('tab', 0)])->with('success', trans('admin/custom_fields/message.field.update.success'));
}
return redirect()->back()->withInput()->with('error', trans('admin/custom_fields/message.field.update.error'));
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Helpers\Helper;
use App\Models\AssetModel;
use App\Models\CustomField;
use App\Models\CustomFieldset;
@@ -38,7 +39,7 @@ class CustomFieldsetsController extends Controller
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v1.8]
*/
public function show($id)
public function show( $id)
{
$cfset = CustomFieldset::with('fields')
->where('id', '=', $id)->orderBy('id', 'ASC')->first();
@@ -46,7 +47,7 @@ class CustomFieldsetsController extends Controller
$this->authorize('view', $cfset);
if ($cfset) {
$custom_fields_list = ['' => 'Add New Field to Fieldset'] + CustomField::pluck('name', 'id')->toArray();
$custom_fields_list = ['' => 'Add New Field to Fieldset'] + CustomField::where('type', $cfset->type)->pluck('name', 'id')->toArray();
$maxid = 0;
foreach ($cfset->fields as $field) {
@@ -96,6 +97,8 @@ class CustomFieldsetsController extends Controller
$fieldset = new CustomFieldset([
'name' => $request->get('name'),
'user_id' => Auth::user()->id,
'type' => Helper::$itemtypes_having_custom_fields[$request->get('tab')]
// 'sub' =>
]);
$validator = Validator::make($request->all(), $fieldset->rules);
+2 -3
View File
@@ -28,8 +28,8 @@ class ProfileController extends Controller
*/
public function getIndex()
{
$this->authorize('self.profile');
$user = Auth::user();
return view('account/profile', compact('user'));
}
@@ -42,7 +42,6 @@ class ProfileController extends Controller
*/
public function postIndex(ImageUploadRequest $request)
{
$this->authorize('self.profile');
$user = Auth::user();
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
@@ -68,7 +67,7 @@ class ProfileController extends Controller
if ($user->save()) {
return redirect()->route('profile')->with('success', trans('account/general.profile_updated'));
return redirect()->route('profile')->with('success', trans('account.general.profile_updated'));
}
return redirect()->back()->withInput()->withErrors($user->getErrors());
+1 -1
View File
@@ -933,7 +933,7 @@ class ReportsController extends Controller
$diff = ($asset->purchase_cost - $depreciation);
$row[] = Helper::formatCurrencyOutput($depreciation);
$row[] = Helper::formatCurrencyOutput($diff);
$row[] = (($asset->depreciation) && ($asset->depreciated_date())) ? $asset->depreciated_date()->format('Y-m-d') : '';
$row[] = ($asset->depreciation) ? $asset->depreciated_date()->format('Y-m-d') : '';
}
if ($request->filled('checkout_date')) {
+57 -66
View File
@@ -25,7 +25,6 @@ use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\URL;
@@ -68,9 +67,28 @@ class SettingsController extends Controller
$start_settings['db_error'] = $e->getMessage();
}
$start_settings['url_config'] = trim(config('app.url'), '/'). '/setup';
$start_settings['real_url'] = request()->url();
$start_settings['url_valid'] = $start_settings['url_config'] === $start_settings['real_url'];
if (array_key_exists("HTTP_X_FORWARDED_PROTO", $_SERVER)) {
$protocol = $_SERVER["HTTP_X_FORWARDED_PROTO"] . "://";
} elseif (array_key_exists('HTTPS', $_SERVER) && ('on' == $_SERVER['HTTPS'])) {
$protocol = "https://";
} else {
$protocol = "http://";
}
if (array_key_exists("HTTP_X_FORWARDED_HOST", $_SERVER)) {
$host = $_SERVER["HTTP_X_FORWARDED_HOST"];
} else {
$host = array_key_exists('SERVER_NAME', $_SERVER) ? $_SERVER['SERVER_NAME'] : null;
$port = array_key_exists('SERVER_PORT', $_SERVER) ? $_SERVER['SERVER_PORT'] : null;
if (('http://' === $protocol && '80' != $port) || ('https://' === $protocol && '443' != $port)) {
$host .= ':'.$port;
}
}
$pageURL = $protocol.$host.$_SERVER['REQUEST_URI'];
$start_settings['url_config'] = config('app.url').'/setup';
$start_settings['url_valid'] = ($start_settings['url_config'] === $pageURL);
$start_settings['real_url'] = $pageURL;
$start_settings['php_version_min'] = true;
// Curl the .env file to make sure it's not accessible via a browser
@@ -107,7 +125,17 @@ class SettingsController extends Controller
$start_settings['owner_is_admin'] = false;
}
$start_settings['writable'] = $this->storagePathIsWritable();
if ((is_writable(storage_path()))
&& (is_writable(storage_path().'/framework'))
&& (is_writable(storage_path().'/framework/cache'))
&& (is_writable(storage_path().'/framework/sessions'))
&& (is_writable(storage_path().'/framework/views'))
&& (is_writable(storage_path().'/logs'))
) {
$start_settings['writable'] = true;
} else {
$start_settings['writable'] = false;
}
$start_settings['gd'] = extension_loaded('gd');
@@ -136,19 +164,6 @@ class SettingsController extends Controller
}
}
/**
* Determine if the app storage path is writable.
*/
protected function storagePathIsWritable(): bool
{
return File::isWritable(storage_path()) &&
File::isWritable(storage_path('framework')) &&
File::isWritable(storage_path('framework/cache')) &&
File::isWritable(storage_path('framework/sessions')) &&
File::isWritable(storage_path('framework/views')) &&
File::isWritable(storage_path('logs'));
}
/**
* Save the first admin user from Setup.
*
@@ -348,11 +363,12 @@ class SettingsController extends Controller
}
$setting->default_eula_text = $request->input('default_eula_text');
$setting->load_remote = $request->input('load_remote', 0);
$setting->thumbnail_max_h = $request->input('thumbnail_max_h');
$setting->privacy_policy_link = $request->input('privacy_policy_link');
$setting->depreciation_method = $request->input('depreciation_method');
$setting->dash_chart_type = $request->input('dash_chart_type');
$setting->profile_edit = $request->input('profile_edit', 0);
if ($request->input('per_page') != '') {
$setting->per_page = $request->input('per_page');
@@ -392,11 +408,10 @@ class SettingsController extends Controller
*
* @since [v1.0]
*
* @return \Illuminate\Contracts\View\View | \Illuminate\Http\RedirectResponse
* @return View
*/
public function postBranding(ImageUploadRequest $request)
{
// Something has gone horribly wrong - no settings record exists!
if (is_null($setting = Setting::getSettings())) {
return redirect()->to('admin')->with('error', trans('admin/settings/message.update.error'));
}
@@ -407,75 +422,51 @@ class SettingsController extends Controller
$setting->version_footer = $request->input('version_footer');
$setting->footer_text = $request->input('footer_text');
$setting->skin = $request->input('skin');
$setting->allow_user_skin = $request->input('allow_user_skin', '0');
$setting->allow_user_skin = $request->input('allow_user_skin');
$setting->show_url_in_emails = $request->input('show_url_in_emails', '0');
$setting->logo_print_assets = $request->input('logo_print_assets', '0');
$setting->load_remote = $request->input('load_remote', 0);
// Only allow the site name, images, and CSS to be changed if lock_passwords is false
// Only allow the site name and CSS to be changed if lock_passwords is false
// Because public demos make people act like dicks
if (!config('app.lock_passwords')) {
if ($request->has('site_name')) {
$request->validate(['site_name' => 'required']);
}
$setting->site_name = $request->input('site_name', 'Snipe-IT');
if (! config('app.lock_passwords')) {
$request->validate(['site_name' => 'required']);
$setting->site_name = $request->input('site_name');
$setting->custom_css = $request->input('custom_css');
// Logo upload
$setting = $request->handleImages($setting, 600, 'logo', '', 'logo');
if ($request->input('clear_logo') == '1') {
if (($setting->logo) && (Storage::exists($setting->logo))) {
Storage::disk('public')->delete($setting->logo);
}
if ('1' == $request->input('clear_logo')) {
Storage::disk('public')->delete($setting->logo);
$setting->logo = null;
$setting->brand = 1;
}
// Email logo upload
$setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo');
if ($request->input('clear_email_logo') == '1') {
if (($setting->email_logo) && (Storage::exists($setting->email_logo))) {
Storage::disk('public')->delete($setting->email_logo);
}
$setting = $request->handleImages($setting, 600, 'email_logo', '', 'email_logo');
if ('1' == $request->input('clear_email_logo')) {
Storage::disk('public')->delete($setting->email_logo);
$setting->email_logo = null;
// If they are uploading an image, validate it and upload it
}
// Label logo upload
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ($request->input('clear_label_logo') == '1') {
if (($setting->label_logo) && (Storage::exists($setting->label_logo))) {
Storage::disk('public')->delete($setting->label_logo);
}
$setting = $request->handleImages($setting, 600, 'label_logo', '', 'label_logo');
if ('1' == $request->input('clear_label_logo')) {
Storage::disk('public')->delete($setting->label_logo);
$setting->label_logo = null;
}
// Favicon upload
$setting = $request->handleImages($setting, 100, 'favicon', '', 'favicon');
$setting = $request->handleImages($setting, 600, 'favicon', '', 'favicon');
// If the user wants to clear the favicon...
if ('1' == $request->input('clear_favicon')) {
if (($setting->favicon) && (Storage::exists($setting->favicon))) {
Storage::disk('public')->delete($setting->favicon);
}
Storage::disk('public')->delete($setting->favicon);
$setting->favicon = null;
}
// Default avatar upload
$setting = $request->handleImages($setting, 500, 'default_avatar', 'avatars', 'default_avatar');
if ($request->input('clear_default_avatar') == '1') {
if (($setting->default_avatar) && (Storage::exists('avatars/'.$setting->default_avatar))) {
Storage::disk('public')->delete('avatars/'.$setting->default_avatar);
}
$setting->default_avatar = null;
}
}
if ($setting->save()) {
@@ -42,7 +42,7 @@ class BulkUsersController extends Controller
// Get the list of affected users
$user_raw_array = request('ids');
$users = User::whereIn('id', $user_raw_array)
->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations','uploads', 'acceptances')->get();
->with('groups', 'assets', 'licenses', 'accessories')->get();
// bulk edit, display the bulk edit form
if ($request->input('bulk_actions') == 'edit') {
@@ -317,7 +317,7 @@ class BulkUsersController extends Controller
// Get the users
$merge_into_user = User::find($request->input('merge_into_id'));
$users_to_merge = User::whereIn('id', $user_ids_to_merge)->with('assets', 'manager', 'userlog', 'licenses', 'consumables', 'accessories', 'managedLocations','uploads', 'acceptances')->get();
$users_to_merge = User::whereIn('id', $user_ids_to_merge)->with('assets', 'licenses', 'consumables','accessories')->get();
$admin = User::find(Auth::user()->id);
// Walk users
@@ -344,20 +344,10 @@ class BulkUsersController extends Controller
}
foreach ($user_to_merge->userlog as $log) {
$log->target_id = $merge_into_user->id;
$log->target_id = $user_to_merge->id;
$log->save();
}
foreach ($user_to_merge->uploads as $upload) {
$upload->item_id = $merge_into_user->id;
$upload->save();
}
foreach ($user_to_merge->acceptances as $acceptance) {
$acceptance->item_id = $merge_into_user->id;
$acceptance->save();
}
User::where('manager_id', '=', $user_to_merge->id)->update(['manager_id' => $merge_into_user->id]);
foreach ($user_to_merge->managedLocations as $managedLocation) {
@@ -366,6 +356,7 @@ class BulkUsersController extends Controller
}
$user_to_merge->delete();
//$user_to_merge->save();
event(new UserMerged($user_to_merge, $merge_into_user, $admin));
+93 -34
View File
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Users;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Requests\DeleteUserRequest;
use App\Http\Requests\ImageUploadRequest;
use App\Http\Requests\SaveUserRequest;
use App\Models\Actionlog;
@@ -17,9 +16,7 @@ use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Storage;
use Redirect;
use Str;
use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -133,6 +130,9 @@ class UsersController extends Controller
// we have to invoke the
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
\Log::info("About to call customFill, in the 'store' controller!!!");
$user->customFill($request, Auth::user());
if ($user->save()) {
if ($request->filled('groups')) {
$user->groups()->sync($request->input('groups'));
@@ -184,7 +184,8 @@ class UsersController extends Controller
{
$this->authorize('update', User::class);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($id);
if ($user) {
@@ -230,7 +231,9 @@ class UsersController extends Controller
$permissions = $request->input('permissions', []);
app('request')->request->set('permissions', $permissions);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($id);
// User is valid - continue...
if ($user) {
@@ -321,7 +324,31 @@ class UsersController extends Controller
}
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id')));
$permissions_array = $request->input('permission');
// Strip out the superuser permission if the user isn't a superadmin
if (!Auth::user()->isSuperUser()) {
unset($permissions_array['superuser']);
$permissions_array['superuser'] = $orig_superuser;
}
$user->permissions = json_encode($permissions_array);
// Handle uploaded avatar
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
\Log::debug("calling custom fill from the UPDATE method!");
$user->customFill($request, Auth::user());
//\Log::debug(print_r($user, true));
// Was the user updated?
if ($user->save()) {
// Redirect to the user page
return redirect()->route('users.index')
->with('success', trans('admin/users/message.success.update'));
}
return redirect()->back()->withInput()->withErrors($user->getErrors());
}
/**
@@ -333,26 +360,55 @@ class UsersController extends Controller
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function destroy(DeleteUserRequest $request, $id = null)
public function destroy($id = null)
{
$this->authorize('delete', User::class);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($id);
if ($user = User::find($id)) {
$this->authorize('delete', $user);
if ($user->delete()) {
if (Storage::disk('public')->exists('avatars/' . $user->avatar)) {
try {
Storage::disk('public')->delete('avatars/' . $user->avatar);
} catch (\Exception $e) {
Log::debug($e);
}
}
return redirect()->route('users.index')->with('success', trans('admin/users/message.success.delete'));
if ($user) {
// Check if we are not trying to delete ourselves
if ($user->id === Auth::id()) {
// Redirect to the user management page
return redirect()->route('users.index')
->with('error', trans('admin/users/message.error.cannot_delete_yourself'));
}
if (($user->assets()) && ($user->assets()->count() > 0)) {
// Redirect to the user management page
return redirect()->route('users.index')
->with('error', trans_choice('admin/users/message.error.delete_has_assets_var', $user->assets()->count(), ['count'=> $user->assets()->count()]));
}
if (($user->licenses()) && ($user->licenses()->count() > 0)) {
return redirect()->route('users.index')->with('error', trans_choice('admin/users/message.error.delete_has_licenses_var', $user->licenses()->count(), ['count'=> $user->licenses()->count()]));
}
if (($user->accessories()) && ($user->accessories()->count() > 0)) {
// Redirect to the user management page
return redirect()->route('users.index')->with('error', trans_choice('admin/users/message.error.delete_has_accessories_var', $user->accessories()->count(), ['count'=> $user->accessories()->count()]));
}
if (($user->managedLocations()) && ($user->managedLocations()->count() > 0)) {
// Redirect to the user management page
return redirect()->route('users.index')
->with('error', trans_choice('admin/users/message.error.delete_has_locations_var', $user->managedLocations()->count(), ['count'=> $user->managedLocations()->count()]));
}
if (($user->managesUsers()) && ($user->managesUsers()->count() > 0)) {
return redirect()->route('users.index')
->with('error', trans_choice('admin/users/message.error.delete_has_users_var', $user->managesUsers()->count(), ['count'=> $user->managesUsers()->count()]));
}
// Delete the user
$user->delete();
return redirect()->route('users.index')->with('success', trans('admin/users/message.success.delete'));
}
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found'));
return redirect()->route('users.index')
->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
@@ -412,7 +468,8 @@ class UsersController extends Controller
// Make sure the user can view users at all
$this->authorize('view', User::class);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($userId);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user = Company::scopeCompanyables($user)->find($userId);
// Make sure they can view this particular user
$this->authorize('view', $user);
@@ -447,7 +504,9 @@ class UsersController extends Controller
app('request')->request->set('permissions', $permissions);
$user_to_clone = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id);
$user_to_clone = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed();
$user_to_clone = Company::scopeCompanyables($user_to_clone)->find($id);
// Make sure they can view this particular user
$this->authorize('view', $user_to_clone);
@@ -511,7 +570,10 @@ class UsersController extends Controller
'groups',
'userloc',
'company'
)->orderBy('created_at', 'DESC')
)->orderBy('created_at', 'DESC');
// FMCS scoping
Company::scopeCompanyables($users)
->chunk(500, function ($users) use ($handle) {
$headers = [
// strtolower to prevent Excel from trying to open it as a SYLK file
@@ -605,21 +667,20 @@ class UsersController extends Controller
public function printInventory($id)
{
$this->authorize('view', User::class);
$user = User::where('id', $id)->withTrashed()->first();
$show_user = Company::scopeCompanyables(User::where('id', $id)->withTrashed()->first());
// Make sure they can view this particular user
$this->authorize('view', $user);
$this->authorize('view', $show_user);
$assets = Asset::where('assigned_to', $id)->where('assigned_type', User::class)->with('model', 'model.category')->get();
$accessories = $user->accessories()->get();
$consumables = $user->consumables()->get();
$accessories = $show_user->accessories()->get();
$consumables = $show_user->consumables()->get();
return view('users/print')->with('assets', $assets)
->with('licenses', $user->licenses()->get())
->with('licenses', $show_user->licenses()->get())
->with('accessories', $accessories)
->with('consumables', $consumables)
->with('show_user', $user)
->with('show_user', $show_user)
->with('settings', Setting::getSettings());
}
@@ -635,7 +696,7 @@ class UsersController extends Controller
{
$this->authorize('view', User::class);
$user = User::find($id);
$user = Company::scopeCompanyables(User::find($id));
// Make sure they can view this particular user
$this->authorize('view', $user);
@@ -663,9 +724,7 @@ class UsersController extends Controller
*/
public function sendPasswordReset($id)
{
$this->authorize('view', User::class);
if (($user = User::find($id)) && ($user->activated == '1') && ($user->email != '') && ($user->ldap_import == '0')) {
if (($user = Company::scopeCompanyables(User::find($id))) && ($user->activated == '1') && ($user->email != '') && ($user->ldap_import == '0')) {
$credentials = ['email' => trim($user->email)];
try {
@@ -1,6 +1,6 @@
<?php
namespace App\Livewire;
namespace App\Http\Livewire;
use Livewire\Component;
@@ -1,7 +1,9 @@
<?php
namespace App\Livewire;
namespace App\Http\Livewire;
use App\Models\Asset;
use App\Models\DefaultValuesForCustomFields;
use Livewire\Component;
use App\Models\CustomFieldset;
@@ -30,7 +32,7 @@ class CustomFieldSetDefaultValuesForModel extends Component
$this->fields = CustomFieldset::find($this->fieldset_id)->fields;
}
$this->add_default_values = ($this->model->defaultValues->count() > 0);
$this->add_default_values = (DefaultValuesForCustomFields::forPivot($this->model, Asset::class)->count() > 0);
}
public function updatedFieldsetId()
@@ -1,6 +1,6 @@
<?php
namespace App\Livewire;
namespace App\Http\Livewire;
use App\Models\CustomField;
use Livewire\Component;
@@ -8,6 +8,7 @@ use Livewire\Component;
use App\Models\Import;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@@ -59,7 +60,7 @@ class Importer extends Component
];
/**
* This is used in resources/views/livewire.importer.blade.php, and we kinda shouldn't need to check for
* This is used in resources/views/livewire/importer.blade.php, and we kinda shouldn't need to check for
* activeFile here, but there's some UI goofiness that allows this to crash out on some imports.
*
* @return string
@@ -108,7 +109,7 @@ class Importer extends Component
if ($type == "asset") {
// add Custom Fields after a horizontal line
$results['-'] = "———" . trans('admin/custom_fields/general.custom_fields') . "———’";
foreach (CustomField::orderBy('name')->get() as $field) {
foreach (CustomField::where('type', \App\Models\Asset::class)->orderBy('name')->get() as $field) { // TODO - generalize?
$results[$field->db_column_name()] = $field->name;
}
}
@@ -118,7 +119,8 @@ class Importer extends Component
public function updating($name, $new_import_type)
{
if ($name == "activeFile.import_type") {
Log::debug("WE ARE CHANGING THE import_type!!!!! TO: " . $new_import_type);
Log::debug("so, what's \$this->>field_map at?: " . print_r($this->field_map, true));
// go through each header, find a matching field to try and map it to.
foreach ($this->activeFile->header_row as $i => $header) {
// do we have something mapped already?
@@ -235,15 +237,6 @@ class Importer extends Component
'email' => trans('general.importer.checked_out_to_email'),
'username' => trans('general.importer.checked_out_to_username'),
'checkout_location' => trans('general.importer.checkout_location'),
/**
* These are here so users can import history, to replace the dinosaur that
* was the history importer
*/
'last_checkin' => trans('admin/hardware/table.last_checkin_date'),
'last_checkout' => trans('admin/hardware/table.checkout_date'),
'expected_checkin' => trans('admin/hardware/form.expected_checkin'),
'last_audit_date' => trans('general.last_audit'),
'next_audit_date' => trans('general.next_audit_date'),
];
$this->consumables_fields = [
@@ -387,12 +380,6 @@ class Importer extends Component
'job title for user',
'job title',
],
'full_name' =>
[
'full name',
'fullname',
trans('general.importer.checked_out_to_fullname')
],
'username' =>
[
'user name',
@@ -425,7 +412,6 @@ class Importer extends Component
'telephone',
'tel.',
],
'serial' =>
[
'serial number',
@@ -470,12 +456,6 @@ class Importer extends Component
[
'Next Audit',
],
'last_checkout' =>
[
'Last Checkout',
'Last Checkout Date',
'Checkout Date',
],
'address2' =>
[
'Address 2',
@@ -543,8 +523,9 @@ class Importer extends Component
{
// TODO: why don't we just do File::find($id)? This seems dumb.
foreach($this->files as $file) {
if ($id == $file->id) {
if (Storage::delete('private_uploads/imports/'.$file->file_path)) {
Log::debug("File id is: ".$file->id);
if($id == $file->id) {
if(Storage::delete('private_uploads/imports/'.$file->file_path)) {
$file->delete();
$this->message = trans('admin/hardware/message.import.file_delete_success');
@@ -1,6 +1,6 @@
<?php
namespace App\Livewire;
namespace App\Http\Livewire;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
@@ -19,11 +19,21 @@ class OauthClients extends Component
public $authorizationError;
protected $clientRepository;
protected $tokenRepository;
public function __construct()
{
$this->clientRepository = app(ClientRepository::class);
$this->tokenRepository = app(TokenRepository::class);
parent::__construct();
}
public function render()
{
return view('livewire.oauth-clients', [
'clients' => app(ClientRepository::class)->activeForUser(auth()->user()->id),
'authorized_tokens' => app(TokenRepository::class)->forUser(auth()->user()->id)->where('revoked', false),
'clients' => $this->clientRepository->activeForUser(auth()->user()->id),
'authorized_tokens' => $this->tokenRepository->forUser(auth()->user()->id)->where('revoked', false),
]);
}
@@ -34,13 +44,13 @@ class OauthClients extends Component
'redirect' => 'required|url|max:255',
]);
app(ClientRepository::class)->create(
$newClient = $this->clientRepository->create(
auth()->user()->id,
$this->name,
$this->redirect,
);
$this->dispatch('clientCreated');
$this->dispatchBrowserEvent('clientCreated');
}
public function deleteClient(Client $clientId): void
@@ -48,7 +58,7 @@ class OauthClients extends Component
// test for safety
// ->delete must be of type Client - thus the model binding
if ($clientId->user_id == auth()->user()->id) {
app(ClientRepository::class)->delete($clientId);
$this->clientRepository->delete($clientId);
} else {
Log::warning('User ' . auth()->user()->id . ' attempted to delete client ' . $clientId->id . ' which belongs to user ' . $clientId->user_id);
$this->authorizationError = 'You are not authorized to delete this client.';
@@ -57,9 +67,9 @@ class OauthClients extends Component
public function deleteToken($tokenId): void
{
$token = app(TokenRepository::class)->find($tokenId);
$token = $this->tokenRepository->find($tokenId);
if ($token->user_id == auth()->user()->id) {
app(TokenRepository::class)->revokeAccessToken($tokenId);
$this->tokenRepository->revokeAccessToken($tokenId);
} else {
Log::warning('User ' . auth()->user()->id . ' attempted to delete token ' . $tokenId . ' which belongs to user ' . $token->user_id);
$this->authorizationError = 'You are not authorized to delete this token.';
@@ -73,7 +83,7 @@ class OauthClients extends Component
$this->editClientId = $editClientId->id;
$this->dispatch('editClient');
$this->dispatchBrowserEvent('editClient');
}
public function updateClient(Client $editClientId): void
@@ -83,7 +93,7 @@ class OauthClients extends Component
'editRedirect' => 'required|url|max:255',
]);
$client = app(ClientRepository::class)->find($editClientId->id);
$client = $this->clientRepository->find($editClientId->id);
if ($client->user_id == auth()->user()->id) {
$client->name = $this->editName;
$client->redirect = $this->editRedirect;
@@ -93,7 +103,7 @@ class OauthClients extends Component
$this->authorizationError = 'You are not authorized to edit this client.';
}
$this->dispatch('clientUpdated');
$this->dispatchBrowserEvent('clientUpdated');
}
}
@@ -1,6 +1,6 @@
<?php
namespace App\Livewire;
namespace App\Http\Livewire;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
@@ -17,7 +17,7 @@ class PersonalAccessTokens extends Component
//this is just an annoying thing to make the modal input autofocus
public function autoFocusModalEvent(): void
{
$this->dispatch('autoFocusModal');
$this->dispatchBrowserEvent('autoFocusModal');
}
public function render()
@@ -42,13 +42,13 @@ class PersonalAccessTokens extends Component
$this->newTokenString = $newToken->accessToken;
$this->dispatch('tokenCreated', token: $newToken->accessToken);
$this->dispatchBrowserEvent('tokenCreated', $newToken->accessToken);
}
public function deleteToken($tokenId): void
{
//this needs safety (though the scope of auth::user might kind of do it...)
//seems like it does, test more
Auth::user()->tokens()->find($tokenId)?->delete();
Auth::user()->tokens()->find($tokenId)->delete();
}
}
@@ -1,6 +1,6 @@
<?php
namespace App\Livewire;
namespace App\Http\Livewire;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Http;
@@ -23,17 +23,15 @@ class SlackSettingsForm extends Component
public Setting $setting;
public $save_button;
public $webhook_test;
public $webhook_endpoint_rules;
protected $rules = [
'webhook_endpoint' => 'required_with:webhook_channel|starts_with:http://,https://,ftp://,irc://,https://hooks.slack.com/services/|url|nullable',
'webhook_channel' => 'required_with:webhook_endpoint|starts_with:#|nullable',
'webhook_botname' => 'string|nullable',
];
public function mount() {
$this->webhook_text= [
+2 -2
View File
@@ -49,10 +49,10 @@ class CheckForTwoFactor
// 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'));
return redirect()->route('two-factor')->with('info', 'Please enter your two-factor authentication code.');
}
return redirect()->route('two-factor-enroll')->with('success', trans('auth/message.two_factor.please_enroll'));
return redirect()->route('two-factor-enroll')->with('success', 'Please enroll a device in two-factor authentication.');
}
}
+2
View File
@@ -34,12 +34,14 @@ class CustomFieldRequest extends FormRequest
case 'POST':
{
$rules['name'] = 'required|unique:custom_fields';
$rules['tab'] = 'required';
break;
}
// Save all fields
case 'PUT':
$rules['name'] = 'required';
$rules['tab'] = 'required';
break;
// Save only what's passed
-99
View File
@@ -1,99 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
class DeleteUserRequest extends FormRequest
{
protected $redirectRoute = 'users.index';
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return Gate::allows('delete', User::class);
}
public function prepareForValidation(): void
{
$user_to_delete = User::withTrashed()->find(request()->route('user'));
if ($user_to_delete) {
$this->merge([
'user' => request()->route('user'),
'admin_id' => Auth::user()->id,
'managed_users' => $user_to_delete->managesUsers()->count(),
'managed_locations' => $user_to_delete->managedLocations()->count(),
'assigned_assets' => $user_to_delete->assets()->count(),
'assigned_licenses' => $user_to_delete->licenses()->count(),
'assigned_accessories' => $user_to_delete->accessories()->count(),
'deleted_at' => $user_to_delete->deleted_at,
]);
}
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'user' => Rule::notIn([auth()->user()->id]),
'managed_users' => Rule::in([0]),
'managed_locations' => Rule::in([0]),
'assigned_assets' => Rule::in([0]),
'assigned_licenses' => Rule::in([0]),
'assigned_accessories' => Rule::in([0]),
'deleted_at' => Rule::in([null]),
];
}
public function messages(): array
{
$user_to_delete = User::withTrashed()->find(request()->route('user'));
$messages = [];
if ($user_to_delete) {
$messages = array_merge([
'user.exists' => trans('admin/users/message.user_not_found'),
// Cannot delete yourself
'user.not_in' => trans('admin/users/message.error.cannot_delete_yourself'),
// managed users is not 0
'managed_users.in' => trans_choice('admin/users/message.error.delete_has_users_var', $user_to_delete->managesUsers()->count(), ['count' => $user_to_delete->managesUsers()->count()]),
// managed locations is not 0
'managed_locations.in' => trans_choice('admin/users/message.error.delete_has_locations_var', $user_to_delete->managedLocations()->count(), ['count' => $user_to_delete->managedLocations()->count()]),
// assigned_assets is not 0
'assigned_assets.in' => trans_choice('admin/users/message.error.delete_has_assets_var', $user_to_delete->assets()->count(), ['count' => $user_to_delete->assets()->count()]),
// assigned licenses is not 0
'assigned_licenses.in' => trans_choice('admin/users/message.error.delete_has_licenses_var', $user_to_delete->licenses()->count(), ['count' => $user_to_delete->licenses()->count()]),
// assigned accessories is not 0
'assigned_accessories.in' => trans_choice('admin/users/message.error.delete_has_accessories_var', $user_to_delete->accessories()->count(), ['count' => $user_to_delete->accessories()->count()]),
'deleted_at.in' => trans('admin/users/message.user_deleted_warning'),
], $messages);
}
return $messages;
}
}
+4 -1
View File
@@ -96,7 +96,10 @@ class ImageUploadRequest extends Request
$ext = $image->guessExtension();
$file_name = $type.'-'.$form_fieldname.'-'.$item->id.'-'.str_random(10).'.'.$ext;
Log::info('File name will be: '.$file_name);
Log::debug('File extension is: '.$ext);
if (($image->getMimeType() == 'image/vnd.microsoft.icon') || ($image->getMimeType() == 'image/x-icon') || ($image->getMimeType() == 'image/avif') || ($image->getMimeType() == 'image/webp')) {
// If the file is an icon, webp or avif, we need to just move it since gd doesn't support resizing
// icons or avif, and webp support and needs to be compiled into gd for resizing to be available
-2
View File
@@ -2,14 +2,12 @@
namespace App\Http\Requests;
use App\Http\Traits\ConvertsBase64ToFiles;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
class UploadFileRequest extends Request
{
use ConvertsBase64ToFiles;
/**
* Determine if the user is authorized to make this request.
*
@@ -49,12 +49,7 @@ class ActionlogsTransformer
public function transformActionlog (Actionlog $actionlog, $settings = null)
{
$icon = $actionlog->present()->icon();
static $custom_fields = false;
if ($custom_fields === false) {
$custom_fields = CustomField::all();
}
$custom_fields = CustomField::all();
if ($actionlog->filename!='') {
$icon = Helper::filetype_icon($actionlog->filename);
@@ -222,30 +217,13 @@ class ActionlogsTransformer
*/
public function changedInfo(array $clean_meta)
{
static $location = false;
static $supplier = false;
static $model = false;
static $status = false;
static $company = false;
{ $location = Location::withTrashed()->get();
$supplier = Supplier::withTrashed()->get();
$model = AssetModel::withTrashed()->get();
$status = Statuslabel::withTrashed()->get();
$company = Company::get();
if ($location === false) {
$location = Location::select('id', 'name')->withTrashed()->get();
}
if ($supplier === false) {
$supplier = Supplier::select('id', 'name')->withTrashed()->get();
}
if ($model === false) {
$model = AssetModel::select('id', 'name')->withTrashed()->get();
}
if ($status === false) {
$status = Statuslabel::select('id', 'name')->withTrashed()->get();
}
if ($company === false) {
$company = Company::select('id', 'name')->get();
}
if(array_key_exists('rtd_location_id',$clean_meta)) {
$oldRtd = $location->find($clean_meta['rtd_location_id']['old']);
+2 -43
View File
@@ -2,6 +2,7 @@
namespace App\Http\Transformers;
use App\Helpers\CustomFieldHelper;
use App\Helpers\Helper;
use App\Models\Asset;
use App\Models\Setting;
@@ -99,49 +100,7 @@ class AssetsTransformer
];
if (($asset->model) && ($asset->model->fieldset) && ($asset->model->fieldset->fields->count() > 0)) {
$fields_array = [];
foreach ($asset->model->fieldset->fields as $field) {
if ($field->isFieldDecryptable($asset->{$field->db_column})) {
$decrypted = Helper::gracefulDecrypt($field, $asset->{$field->db_column});
$value = (Gate::allows('assets.view.encrypted_custom_fields')) ? $decrypted : strtoupper(trans('admin/custom_fields/general.encrypted'));
if ($field->format == 'DATE'){
if (Gate::allows('assets.view.encrypted_custom_fields')){
$value = Helper::getFormattedDateObject($value, 'date', false);
} else {
$value = strtoupper(trans('admin/custom_fields/general.encrypted'));
}
}
$fields_array[$field->name] = [
'field' => e($field->db_column),
'value' => e($value),
'field_format' => $field->format,
'element' => $field->element,
];
} else {
$value = $asset->{$field->db_column};
if (($field->format == 'DATE') && (!is_null($value)) && ($value!='')){
$value = Helper::getFormattedDateObject($value, 'date', false);
}
$fields_array[$field->name] = [
'field' => e($field->db_column),
'value' => e($value),
'field_format' => $field->format,
'element' => $field->element,
];
}
$array['custom_fields'] = $fields_array;
}
} else {
$array['custom_fields'] = new \stdClass; // HACK to force generation of empty object instead of empty list
}
$array['custom_fields'] = CustomFieldHelper::transform($asset->model->fieldset,$asset);
$permissions_array['available_actions'] = [
'checkout' => ($asset->deleted_at=='' && Gate::allows('checkout', Asset::class)) ? true : false,
@@ -3,6 +3,8 @@
namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\CustomFieldset;
use Illuminate\Database\Eloquent\Collection;
@@ -21,8 +23,13 @@ class CustomFieldsetsTransformer
public function transformCustomFieldset(CustomFieldset $fieldset)
{
$fields = $fieldset->fields;
$models = $fieldset->models;
$models = [];
$modelsArray = [];
if ($fieldset->type == Asset::class) {
\Log::debug("Item pivot id is: ".$fieldset->item_pivot_id);
$models = AssetModel::where('fieldset_id', $fieldset->id)->get();
\Log::debug("And the models object count is: ".$models->count());
}
foreach ($models as $model) {
$modelsArray[] = [
@@ -30,15 +37,21 @@ class CustomFieldsetsTransformer
'name' => e($model->name),
];
}
\Log::debug("Models array is: ".print_r($modelsArray,true));
$array = [
'id' => (int) $fieldset->id,
'name' => e($fieldset->name),
'fields' => (new CustomFieldsTransformer)->transformCustomFields($fields, $fieldset->fields_count),
'models' => (new DatatablesTransformer)->transformDatatables($modelsArray, $fieldset->models_count),
'customizables' => (new DatatablesTransformer)->transformDatatables($fieldset->customizables(),count($fieldset->customizables())),
'created_at' => Helper::getFormattedDateObject($fieldset->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($fieldset->updated_at, 'datetime'),
'type' => $fieldset->type,
];
if ($fieldset->type == Asset::class) {
// TODO - removeme - legacy column just for Assets?
$array['models'] = (new DatatablesTransformer)->transformDatatables($modelsArray, count($modelsArray));
}
return $array;
}
+6 -1
View File
@@ -2,7 +2,10 @@
namespace App\Http\Transformers;
use App\Helpers\CustomFieldHelper;
use App\Helpers\Helper;
use App\Models\CustomField;
use App\Models\CustomFieldset;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
@@ -24,7 +27,7 @@ class UsersTransformer
$array = [
'id' => (int) $user->id,
'avatar' => e($user->present()->gravatar) ?? null,
'avatar' => e($user->present()->gravatar),
'name' => e($user->getFullNameAttribute()),
'first_name' => e($user->first_name),
'last_name' => e($user->last_name),
@@ -80,6 +83,8 @@ class UsersTransformer
'deleted_at' => ($user->deleted_at) ? Helper::getFormattedDateObject($user->deleted_at, 'datetime') : null,
];
$array['custom_fields'] = CustomFieldHelper::transform(CustomFieldset::where('type',User::class)->first(), $user);
$permissions_array['available_actions'] = [
'update' => (Gate::allows('update', User::class) && ($user->deleted_at == '')),
'delete' => $user->isDeletable(),
+27 -65
View File
@@ -7,10 +7,8 @@ use App\Models\AssetModel;
use App\Models\Statuslabel;
use App\Models\User;
use App\Events\CheckoutableCheckedIn;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
class AssetImporter extends ItemImporter
{
@@ -20,10 +18,8 @@ class AssetImporter extends ItemImporter
{
parent::__construct($filename);
$this->defaultStatusLabelId = Statuslabel::first()->id;
if (!is_null(Statuslabel::deployable()->first())) {
$this->defaultStatusLabelId = Statuslabel::deployable()->first()->id;
if (!is_null(Statuslabel::first())) {
$this->defaultStatusLabelId = Statuslabel::first()->id;
}
}
@@ -32,9 +28,10 @@ class AssetImporter extends ItemImporter
// ItemImporter handles the general fetching.
parent::handle($row);
// FIXME : YUP!!!!! This shit needs to go (?) Yeah?
if ($this->customFields) {
foreach ($this->customFields as $customField) {
$customFieldValue = $this->array_smart_custom_field_fetch($row, $customField);
$customFieldValue = $this->array_smart_custom_field_fetch($row, $customField); // TODO/FIXME - this might require a new 'mode' on customFill()?
if ($customFieldValue) {
if ($customField->field_encrypted == 1) {
@@ -44,7 +41,7 @@ class AssetImporter extends ItemImporter
$this->item['custom_fields'][$customField->db_column_name()] = $customFieldValue;
$this->log('Custom Field '.$customField->name.': '.$customFieldValue);
}
} else {
} else { // FIXME - think this through? Do we want to blank this? Is that how other stuff works?
// Clear out previous data.
$this->item['custom_fields'][$customField->db_column_name()] = null;
}
@@ -68,14 +65,16 @@ class AssetImporter extends ItemImporter
$editingAsset = false;
$asset_tag = $this->findCsvMatch($row, 'asset_tag');
if (empty($asset_tag)){
if(empty($asset_tag)){
$asset_tag = Asset::autoincrement_asset();
}
$asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first();
if ($asset) {
if (! $this->updating) {
$this->log('A matching Asset '.$asset_tag.' already exists');
return;
}
@@ -85,13 +84,6 @@ class AssetImporter extends ItemImporter
$this->log('No Matching Asset, Creating a new one');
$asset = new Asset;
}
// If no status ID is found
if (! array_key_exists('status_id', $this->item) && ! $editingAsset) {
$this->log('No status ID field found, defaulting to first deployable status label.');
$this->item['status_id'] = $this->defaultStatusLabelId;
}
$this->item['notes'] = trim($this->findCsvMatch($row, 'asset_notes'));
$this->item['image'] = trim($this->findCsvMatch($row, 'image'));
$this->item['requestable'] = trim(($this->fetchHumanBoolean($this->findCsvMatch($row, 'requestable'))) == 1) ? '1' : 0;
@@ -99,12 +91,14 @@ class AssetImporter extends ItemImporter
$this->item['warranty_months'] = intval(trim($this->findCsvMatch($row, 'warranty_months')));
$this->item['model_id'] = $this->createOrFetchAssetModel($row);
$this->item['byod'] = ($this->fetchHumanBoolean(trim($this->findCsvMatch($row, 'byod'))) == 1) ? '1' : 0;
$this->item['last_checkin'] = trim($this->findCsvMatch($row, 'last_checkin'));
$this->item['last_checkout'] = trim($this->findCsvMatch($row, 'last_checkout'));
$this->item['expected_checkin'] = trim($this->findCsvMatch($row, 'expected_checkin'));
$this->item['last_audit_date'] = trim($this->findCsvMatch($row, 'last_audit_date'));
$this->item['next_audit_date'] = trim($this->findCsvMatch($row, 'next_audit_date'));
$this->item['asset_eol_date'] = trim($this->findCsvMatch($row, 'asset_eol_date'));
// If no status ID is found
if (! array_key_exists('status_id', $this->item) && ! $editingAsset) {
$this->log('No status field found, defaulting to first status.');
$this->item['status_id'] = $this->defaultStatusLabelId;
}
$this->item['asset_tag'] = $asset_tag;
// We need to save the user if it exists so that we can checkout to user later.
@@ -112,9 +106,7 @@ class AssetImporter extends ItemImporter
if (array_key_exists('checkout_target', $this->item)) {
$target = $this->item['checkout_target'];
}
$item = $this->sanitizeItemForStoring($asset, $editingAsset);
// The location id fetched by the csv reader is actually the rtd_location_id.
// This will also set location_id, but then that will be overridden by the
// checkout method if necessary below.
@@ -122,42 +114,16 @@ class AssetImporter extends ItemImporter
$item['rtd_location_id'] = $this->item['location_id'];
}
/**
* We use this to backdate the checkin action further down
*/
$checkin_date = date('Y-m-d H:i:s');
if ($this->item['last_checkin']!='') {
$item['last_checkin'] = $this->parseOrNullDate('last_checkin', 'datetime');
$checkout_date = $this->item['last_checkin'];
$item['last_audit_date'] = null;
if (isset($this->item['last_audit_date'])) {
$item['last_audit_date'] = $this->item['last_audit_date'];
}
/**
* We use this to backdate the checkout action further down
*/
$checkout_date = date('Y-m-d H:i:s');
if ($this->item['last_checkout']!='') {
$item['last_checkout'] = $this->parseOrNullDate('last_checkout', 'datetime');
$checkout_date = $this->item['last_checkout'];
$item['next_audit_date'] = null;
if (isset($this->item['next_audit_date'])) {
$item['next_audit_date'] = $this->item['next_audit_date'];
}
if ($this->item['expected_checkin']!='') {
$item['expected_checkin'] = $this->parseOrNullDate('expected_checkin');
}
if ($this->item['last_audit_date']!='') {
$item['last_audit_date'] = $this->parseOrNullDate('last_audit_date');
}
if ($this->item['next_audit_date']!='') {
$item['next_audit_date'] = $this->parseOrNullDate('next_audit_date');
}
if ($this->item['asset_eol_date']!='') {
$item['asset_eol_date'] = $this->parseOrNullDate('asset_eol_date');
}
if ($editingAsset) {
$asset->update($item);
} else {
@@ -170,31 +136,27 @@ class AssetImporter extends ItemImporter
$asset->{$custom_field} = $val;
}
}
// This sets an attribute on the Loggable trait for the action log
$asset->setImported(true);
if ($asset->save()) {
$this->log('Asset '.$this->item['name'].' with serial number '.$this->item['serial'].' was created');
// If we have a target to checkout to, lets do so.
//-- user_id is a property of the abstract class Importer, which this class inherits from and it's set by
//-- user_id is a property of the abstract class Importer, which this class inherits from and it's setted by
//-- the class that needs to use it (command importer or GUI importer inside the project).
if (isset($target) && ($target !== false)) {
if (!is_null($asset->assigned_to)){
if ($asset->assigned_to != $target->id) {
event(new CheckoutableCheckedIn($asset, User::find($asset->assigned_to), Auth::user(), 'Checkin from CSV Importer', $checkin_date));
if ($asset->assigned_to != $target->id){
event(new CheckoutableCheckedIn($asset, User::find($asset->assigned_to), Auth::user(), $asset->notes, date('Y-m-d H:i:s')));
}
}
$asset->fresh()->checkOut($target, $this->user_id, $checkout_date, null, 'Checkout from CSV Importer', $asset->name);
$asset->fresh()->checkOut($target, $this->user_id, date('Y-m-d H:i:s'), null, $asset->notes, $asset->name);
}
return;
}
$this->logError($asset, 'Asset "'.$this->item['name'].'"');
}
}
+1 -33
View File
@@ -6,7 +6,6 @@ use App\Models\CustomField;
use App\Models\Department;
use App\Models\Setting;
use App\Models\User;
use Carbon\CarbonImmutable;
use ForceUTF8\Encoding;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
@@ -176,7 +175,7 @@ abstract class Importer
* @author Daniel Meltzer
* @since 5.0
*/
protected function populateCustomFields($headerRow)
protected function populateCustomFields($headerRow) // FIXME - what in the actual fuck is this.
{
// Stolen From https://adamwathan.me/2016/07/14/customizing-keys-when-mapping-collections/
// This 'inverts' the fields such that we have a collection of fields indexed by name.
@@ -552,35 +551,4 @@ abstract class Importer
return null;
}
/**
* Parse a date or return null
*
* @author A. Gianotto
* @since 7.0.0
* @param $field
* @param $format
* @return string|null
*/
public function parseOrNullDate($field, $format = 'date') {
$date_format = 'Y-m-d';
if ($format == 'datetime') {
$date_format = 'Y-m-d H:i:s';
}
if (array_key_exists($field, $this->item) && $this->item[$field] != '') {
try {
$value = CarbonImmutable::parse($this->item[$field])->format($date_format);
return $value;
} catch (\Exception $e) {
$this->log('Unable to parse date: ' . $this->item[$field]);
return null;
}
}
return null;
}
}
+20 -12
View File
@@ -79,18 +79,26 @@ class ItemImporter extends Importer
$this->item['purchase_date'] = date('Y-m-d', strtotime($this->findCsvMatch($row, 'purchase_date')));
}
// $this->item['asset_eol_date'] = null;
// if ($this->findCsvMatch($row, 'asset_eol_date') != '') {
// $csvMatch = $this->findCsvMatch($row, 'asset_eol_date');
// \Log::warning('EOL Date for $csvMatch is '.$csvMatch);
// try {
// $this->item['asset_eol_date'] = CarbonImmutable::parse($csvMatch)->format('Y-m-d');
// } catch (\Exception $e) {
// Log::info($e->getMessage());
// $this->log('Unable to parse date: '.$csvMatch);
// }
// }
$this->item['last_audit_date'] = null;
if ($this->findCsvMatch($row, 'last_audit_date') != '') {
$this->item['last_audit_date'] = date('Y-m-d', strtotime($this->findCsvMatch($row, 'last_audit_date')));
}
$this->item['next_audit_date'] = null;
if ($this->findCsvMatch($row, 'next_audit_date') != '') {
$this->item['next_audit_date'] = date('Y-m-d', strtotime($this->findCsvMatch($row, 'next_audit_date')));
}
$this->item['asset_eol_date'] = null;
if($this->findCsvMatch($row, 'asset_eol_date') != '') {
$csvMatch = $this->findCsvMatch($row, 'asset_eol_date');
try {
$this->item['asset_eol_date'] = CarbonImmutable::parse($csvMatch)->format('Y-m-d');
} catch (\Exception $e) {
Log::info($e->getMessage());
$this->log('Unable to parse date: '.$csvMatch);
}
}
$this->item['qty'] = $this->findCsvMatch($row, 'quantity');
$this->item['requestable'] = $this->findCsvMatch($row, 'requestable');
@@ -381,6 +389,7 @@ class ItemImporter extends Importer
if ($status->save()) {
$this->log('Status '.$asset_statuslabel_name.' was created');
return $status->id;
}
@@ -500,5 +509,4 @@ class ItemImporter extends Importer
return null;
}
}
+2 -2
View File
@@ -111,7 +111,7 @@ class LogListener
$logaction->target_type = User::class;
$logaction->action_type = 'merged';
$logaction->note = trans('general.merged_log_this_user_from', $to_from_array);
$logaction->user_id = $event->admin->id ?? null;
$logaction->user_id = $event->admin->id;
$logaction->save();
// Add a record to the users being merged TO
@@ -122,7 +122,7 @@ class LogListener
$logaction->item_type = User::class;
$logaction->action_type = 'merged';
$logaction->note = trans('general.merged_log_this_user_into', $to_from_array);
$logaction->user_id = $event->admin->id ?? null;
$logaction->user_id = $event->admin->id;
$logaction->save();
+42 -147
View File
@@ -8,21 +8,20 @@ use App\Exceptions\CheckoutNotAllowed;
use App\Helpers\Helper;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Traits\Acceptable;
use App\Models\Traits\Customizable;
use App\Models\Traits\HasCustomFields;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use App\Presenters\AssetPresenter;
use AssetPresenter;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
/**
* Model for Assets.
@@ -32,7 +31,7 @@ use Illuminate\Database\Eloquent\Model;
class Asset extends Depreciable
{
protected $presenter = AssetPresenter::class;
protected $presenter = \App\Presenters\AssetPresenter::class;
use CompanyableTrait;
use HasFactory, Loggable, Requestable, Presentable, SoftDeletes, ValidatingTrait, UniqueUndeletedTrait;
@@ -41,8 +40,21 @@ class Asset extends Depreciable
public const ASSET = 'asset';
public const USER = 'user';
use Acceptable;
use Acceptable, HasCustomFields;
public function getFieldsetKey(): object|int|null
{
return $this->model;
}
public static function getFieldsetUsers(int $fieldset_id): array
{
$models = [];
foreach (AssetModel::where("fieldset_id", $fieldset_id)->get() as $model) {
$models[route('models.show', $model->id)] = $model->name . (($model->model_number) ? ' (' . $model->model_number . ')' : '');
}
return $models;
}
/**
* Run after the checkout acceptance was declined by the user
*
@@ -64,12 +76,6 @@ class Asset extends Depreciable
*/
protected $table = 'assets';
/**
* Leaving this commented out, since we need to test further, but this would eager load the model relationship every single
* time the asset model is loaded.
*/
// protected $with = ['model'];
/**
* Whether the model should inject it's identifier to the unique
* validation rules before attempting validation. If this property
@@ -155,33 +161,31 @@ class Asset extends Depreciable
'expected_checkin',
'byod',
'asset_eol_date',
'eol_explicit',
'eol_explicit',
'last_audit_date',
'next_audit_date',
'asset_eol_date',
'last_checkin',
'last_checkout',
];
use Searchable;
/**
* The attributes that should be included when searching the model.
*
*
* @var array
*/
protected $searchableAttributes = [
'name',
'asset_tag',
'serial',
'order_number',
'purchase_cost',
'notes',
'name',
'asset_tag',
'serial',
'order_number',
'purchase_cost',
'notes',
'created_at',
'updated_at',
'purchase_date',
'expected_checkin',
'next_audit_date',
'updated_at',
'purchase_date',
'expected_checkin',
'next_audit_date',
'last_audit_date',
'last_checkin',
'last_checkout',
@@ -190,7 +194,7 @@ class Asset extends Depreciable
/**
* The relations and their attributes that should be included when searching the model.
*
*
* @var array
*/
protected $searchableRelations = [
@@ -213,40 +217,6 @@ class Asset extends Depreciable
$this->attributes['expected_checkin'] = $value;
}
/**
* This handles the custom field validation for assets
*
* @var array
*/
public function save(array $params = [])
{
if ($this->model_id != '') {
$model = AssetModel::find($this->model_id);
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field){
if($field->format == 'BOOLEAN'){
$this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN);
}
}
$this->rules += $model->fieldset->validation_rules();
if ($this->model->fieldset){
foreach ($this->model->fieldset->fields as $field){
if($field->format == 'BOOLEAN'){
$this->{$field->db_column} = filter_var($this->{$field->db_column}, FILTER_VALIDATE_BOOLEAN);
}
}
}
}
}
return parent::save($params);
}
public function getDisplayNameAttribute()
{
return $this->present()->name();
@@ -303,7 +273,7 @@ class Asset extends Depreciable
// The asset status is not archived and is deployable
if (($this->assetstatus) && ($this->assetstatus->archived == '0')
&& ($this->assetstatus->deployable == '1'))
&& ($this->assetstatus->deployable == '1'))
{
return true;
@@ -576,7 +546,7 @@ class Asset extends Depreciable
*/
public function assignedType()
{
return $this->assigned_type ? strtolower(class_basename($this->assigned_type)) : null;
return strtolower(class_basename($this->assigned_type));
}
@@ -860,11 +830,11 @@ class Asset extends Depreciable
foreach ($assets as $asset) {
$results = preg_match("/\d+$/", $asset['asset_tag'], $matches);
if ($results)
if ($results)
{
$number = $matches[0];
if ($number > $max)
if ($number > $max)
{
$max = $number;
}
@@ -968,46 +938,6 @@ class Asset extends Depreciable
return $cost;
}
public function handleCustomFieldsForStoring($request) : Asset
{
$model = AssetModel::find($this->model_id);
if (($model) && ($model instanceof AssetModel) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
/*
* Check if the decrypted existing value is different from one we just submitted
* and if not, pull it out of the object since it shouldn't really be updating at all.
* If we don't do this, it will try to re-encrypt it, and the same value encrypted two
* different times will have different values, so it will *look* like it was updated
* but it wasn't.
*/
if ($request->input($field->db_column)!='') {
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (is_array($request->input($field->db_column))) {
$this->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
$this->{$field->db_column} = Crypt::encrypt($request->input($field->db_column));
}
}
} else {
if (is_array($request->input($field->db_column))) {
$this->{$field->db_column} = implode(', ', $request->input($field->db_column));
} else {
$this->{$field->db_column} = $request->input($field->db_column);
}
}
}
}
}
return $this;
}
/**
* -----------------------------------------------
* BEGIN MUTATORS
@@ -1023,45 +953,14 @@ class Asset extends Depreciable
* @param $value
* @return void
*/
protected function nextAuditDate(): Attribute
public function getNextAuditDateAttribute($value)
{
return Attribute::make(
get: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d') : null,
set: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d') : null,
);
return $this->attributes['next_audit_date'] = $value ? Carbon::parse($value)->format('Y-m-d') : null;
}
protected function lastAuditDate(): Attribute
public function setNextAuditDateAttribute($value)
{
return Attribute::make(
get: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d H:i:s') : null,
set: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d H:i:s') : null,
);
}
protected function lastCheckout(): Attribute
{
return Attribute::make(
get: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d H:i:s') : null,
set: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d H:i:s') : null,
);
}
protected function lastCheckin(): Attribute
{
return Attribute::make(
get: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d H:i:s') : null,
set: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d H:i:s') : null,
);
}
protected function assetEolDate(): Attribute
{
return Attribute::make(
get: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d') : null,
set: fn ($value) => $value ? Carbon::parse($value)->format('Y-m-d') : null,
);
$this->attributes['next_audit_date'] = $value ? Carbon::parse($value)->format('Y-m-d') : null;
}
/**
@@ -1073,13 +972,9 @@ class Asset extends Depreciable
* @param $value
* @return void
*/
protected function requestable(): Attribute
public function setRequestableAttribute($value)
{
return Attribute::make(
get: fn ($value) => (int) filter_var($value, FILTER_VALIDATE_BOOLEAN),
set: fn ($value) => (int) filter_var($value, FILTER_VALIDATE_BOOLEAN),
);
$this->attributes['requestable'] = (int) filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
+1 -12
View File
@@ -142,6 +142,7 @@ class AssetModel extends SnipeModel
*/
public function fieldset()
{
// this is actually OK - we don't *need* to do this, but it's okay to make references from Model to fieldset
return $this->belongsTo(\App\Models\CustomFieldset::class, 'fieldset_id');
}
@@ -150,18 +151,6 @@ class AssetModel extends SnipeModel
return $this->fieldset()->first()->fields();
}
/**
* Establishes the model -> custom field default values relationship
*
* @author hannah tinkler
* @since [v4.3]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function defaultValues()
{
return $this->belongsToMany(\App\Models\CustomField::class, 'models_custom_fields')->withPivot('default_value');
}
/**
* Gets the full url for the image
*
+12 -16
View File
@@ -5,11 +5,11 @@ namespace App\Models;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\Gate;
use Watson\Validating\ValidatingTrait;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
/**
* Model for Companies.
*
@@ -147,7 +147,7 @@ final class Company extends SnipeModel
// This is primary for the gate:allows-check in location->isDeletable()
// Locations don't have a company_id so without this it isn't possible to delete locations with FullMultipleCompanySupport enabled
// because this function is called by SnipePermissionsPolicy->before()
if (!$companyable instanceof Company && !Schema::hasColumn($company_table, 'company_id')) {
if (!$companyable instanceof Company && !\Schema::hasColumn($company_table, 'company_id')) {
return true;
}
@@ -259,7 +259,7 @@ final class Company extends SnipeModel
public static function scopeCompanyables($query, $column = 'company_id', $table_name = null)
{
// If not logged in and hitting this, assume we are on the command line and don't scope?'
if (! static::isFullMultipleCompanySupportEnabled() || (Auth::hasUser() && Auth::user()->isSuperUser()) || (! Auth::hasUser())) {
if (! static::isFullMultipleCompanySupportEnabled() || (Auth::check() && Auth::user()->isSuperUser()) || (! Auth::check())) {
return $query;
} else {
return static::scopeCompanyablesDirectly($query, $column, $table_name);
@@ -267,31 +267,27 @@ final class Company extends SnipeModel
}
/**
* Scoping table queries, determining if a logged-in user is part of a company, and only allows
* Scoping table queries, determining if a logged in user is part of a company, and only allows
* that user to see items associated with that company
*
* @see https://github.com/laravel/framework/pull/24518 for info on Auth::hasUser()
*/
private static function scopeCompanyablesDirectly($query, $column = 'company_id', $table_name = null)
{
// Get the company ID of the logged-in user, or set it to null if there is no company associated with the user
if (Auth::hasUser()) {
// Get the company ID of the logged in user, or set it to null if there is no company assicoated with the user
if (Auth::user()) {
$company_id = Auth::user()->company_id;
} else {
$company_id = null;
}
// Dynamically get the table name if it's not passed in, based on the model we're querying against
$table = ($table_name) ? $table_name."." : $query->getModel()->getTable().".";
// If the column exists in the table, use it to scope the query
if ((($query) && ($query->getModel()) && (Schema::hasColumn($query->getModel()->getTable(), $column)))) {
// Dynamically get the table name if it's not passed in, based on the model we're querying against
$table = ($table_name) ? $table_name."." : $query->getModel()->getTable().".";
if (\Schema::hasColumn($query->getModel()->getTable(), $column)) {
return $query->where($table.$column, '=', $company_id);
} else {
return $query->join('users as users_comp', 'users_comp.id', 'user_id')->where('users_comp.company_id', '=', $company_id);
}
}
/**
@@ -309,7 +305,7 @@ final class Company extends SnipeModel
if (count($companyable_names) == 0) {
throw new Exception('No Companyable Children to scope');
} elseif (! static::isFullMultipleCompanySupportEnabled() || (Auth::hasUser() && Auth::user()->isSuperUser())) {
} elseif (! static::isFullMultipleCompanySupportEnabled() || (Auth::check() && Auth::user()->isSuperUser())) {
return $query;
} else {
$f = function ($q) {
+29 -42
View File
@@ -16,7 +16,7 @@ class CustomField extends Model
UniqueUndeletedTrait;
/**
* Custom field predfined formats
* Custom field predefined formats
*
* @var array
*/
@@ -82,30 +82,19 @@ class CustomField extends Model
'show_in_requestable_list',
];
/**
* This is confusing, since it's actually the custom fields table that
* we're usually modifying, but since we alter the assets table, we have to
* say that here, otherwise the new fields get added onto the custom fields
* table instead of the assets table.
*
* @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v3.0]
*/
public static $table_name = 'assets';
/**
* Convert the custom field's name property to a db-safe string.
*
* We could probably have used str_slug() here but not sure what it would
* do with previously existing values. - @snipe
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4]
* @return string
* @since [v3.4]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public static function name_to_db_name($name)
{
return '_snipeit_'.preg_replace('/[^a-zA-Z0-9]/', '_', strtolower($name));
return '_snipeit_' . preg_replace('/[^a-zA-Z0-9]/', '_', strtolower($name));
}
/**
@@ -116,23 +105,22 @@ class CustomField extends Model
* if they have changed, so we handle that here so that we don't have to remember
* to do it in the controllers.
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.4]
* @return bool
* @since [v3.4]
* @author [A. Gianotto] [<snipe@snipe.net>]
*/
public static function boot()
{
parent::boot();
self::created(function ($custom_field) {
// Column already exists on the assets table - nothing to do here.
// This *shouldn't* happen in the wild.
if (Schema::hasColumn(self::$table_name, $custom_field->db_column)) {
if (Schema::hasColumn($custom_field->getTableName(), $custom_field->db_column)) {
return false;
}
// Update the column name in the assets table
Schema::table(self::$table_name, function ($table) use ($custom_field) {
Schema::table($custom_field->getTableName(), function ($table) use ($custom_field) {
$table->text($custom_field->convertUnicodeDbSlug())->nullable();
});
@@ -145,7 +133,7 @@ class CustomField extends Model
// Column already exists on the assets table - nothing to do here.
if ($custom_field->isDirty('name')) {
if (Schema::hasColumn(self::$table_name, $custom_field->convertUnicodeDbSlug())) {
if (Schema::hasColumn($custom_field->getTableName(), $custom_field->convertUnicodeDbSlug())) {
return true;
}
@@ -155,7 +143,7 @@ class CustomField extends Model
$platform->registerDoctrineTypeMapping('enum', 'string');
// Rename the field if the name has changed
Schema::table(self::$table_name, function ($table) use ($custom_field) {
Schema::table($custom_field->getTableName(), function ($table) use ($custom_field) {
$table->renameColumn($custom_field->convertUnicodeDbSlug($custom_field->getOriginal('name')), $custom_field->convertUnicodeDbSlug());
});
@@ -171,12 +159,19 @@ class CustomField extends Model
// Drop the assets column if we've deleted it from custom fields
self::deleting(function ($custom_field) {
return Schema::table(self::$table_name, function ($table) use ($custom_field) {
return Schema::table($custom_field->getTableName(), function ($table) use ($custom_field) {
$table->dropColumn($custom_field->db_column);
});
});
}
public function getTableName()
{
$type = $this->type;
$instance = new $type();
return $instance->getTable();
}
/**
* Establishes the customfield -> fieldset relationship
*
@@ -207,31 +202,23 @@ class CustomField extends Model
}
/**
* Establishes the customfield -> default values relationship
*
* @author Hannah Tinkler
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function defaultValues()
{
return $this->belongsToMany(\App\Models\AssetModel::class, 'models_custom_fields')->withPivot('default_value');
}
/**
* Returns the default value for a given model using the defaultValues
* Returns the default value for a given 'item' using the defaultValues
* relationship
*
* @param int $modelId
* @return string
*/
public function defaultValue($modelId)
public function defaultValue($pivot_id)
{
return $this->defaultValues->filter(function ($item) use ($modelId) {
return $item->pivot->asset_model_id == $modelId;
})->map(function ($item) {
return $item->pivot->default_value;
})->first();
/*
below, you might think you need to add:
where('type', $this->type),
but the type can be inferred from by the custom_field itself (which also has a type)
can't use forPivot() here because we don't have an object yet. (TODO?)
*/
DefaultValuesForCustomFields::where('item_pivot_id', $pivot_id)->where('custom_field_id', $this->id)->first()?->default_value; //TODO - php8-only operator!
}
/**
+13 -4
View File
@@ -22,6 +22,7 @@ class CustomFieldset extends Model
*/
public $rules = [
'name' => 'required|unique:custom_fieldsets',
''
];
/**
@@ -50,11 +51,13 @@ class CustomFieldset extends Model
*
* @author [Brady Wetherington] [<uberbrady@gmail.com>]
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
* @return \Illuminate\Database\Eloquent\Collection
*/
public function models()
public function customizables() // TODO - I don't like this name, but I can't think of anything better
{
return $this->hasMany(\App\Models\AssetModel::class, 'fieldset_id');
$customizable_class_name = $this->type; //TODO - copypasta from Customizable trait?
\Log::debug("Customizable Class name is: ".$customizable_class_name);
return $customizable_class_name::getFieldsetUsers($this->id);
}
/**
@@ -79,6 +82,7 @@ class CustomFieldset extends Model
*/
public function validation_rules()
{
\Log::debug("CALLING validation_rules FOR customfiledsets!");
$rules = [];
foreach ($this->fields as $field) {
$rule = [];
@@ -92,7 +96,12 @@ class CustomFieldset extends Model
$rule[] = 'unique_undeleted';
}
array_push($rule, $field->attributes['format']);
\Log::debug("Field Format for".$field->name." is: ".$field->format);
if($field->format == 'DATE') { //we do a weird mutator thing, it's confusing - but, yes, it's all-caps
$rule[] = 'date_format:Y-m-d';
} else {
array_push($rule, $field->attributes['format']);
}
$rules[$field->db_column_name()] = $rule;
// add not_array to rules for all fields but checkboxes
@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Watson\Validating\ValidatingTrait;
class DefaultValuesForCustomFields extends Model
{
use HasFactory, ValidatingTrait;
protected $rules = [
'type' => 'required'
];
public $timestamps = false;
public function field() {
return $this->belongsTo('custom_fields');
}
// There is, effectively, another 'relation' here, but it's weirdly polymorphic
// and impossible to represent in Laravel.
// we have a 'type', and we have an 'item_pivot_id' -
// For example, in Assets the 'type' would be App\Models\Asset, and the 'item_pivot_id' would be a model_id
// I can't come up with any way to represent this in Laravel/Eloquent
// TODO: might be getting overly-fancy here; maybe just want to do an ID? Instead of an Eloquent Model?
public function scopeForPivot(Builder $query, Model $item, string $class) {
return $query->where('item_pivot_id', $item->id)->where('type', $class);
}
}
+3 -7
View File
@@ -173,14 +173,10 @@ class Depreciable extends SnipeModel
public function depreciated_date()
{
if (($this->purchase_date) && ($this->get_depreciation())) {
$date = date_create($this->purchase_date);
return date_add($date, date_interval_create_from_date_string($this->get_depreciation()->months.' months'));//date_format($date, 'Y-m-d'); //don't bake-in format, for internationalization
}
return null;
$date = date_create($this->purchase_date);
date_add($date, date_interval_create_from_date_string($this->get_depreciation()->months.' months'));
return $date; //date_format($date, 'Y-m-d'); //don't bake-in format, for internationalization
}
// it's necessary for unit tests
-11
View File
@@ -25,17 +25,6 @@ abstract class Label
*/
public abstract function getUnit();
/**
* Returns the PDF rotation.
* 0, 90, 180, 270
* 0 is a sane default. Override when necessary.
*
* @return int
*/
public function getRotation() {
return 0;
}
/**
* Returns the label's width in getUnit() units
*
@@ -1,23 +0,0 @@
<?php
namespace App\Models\Labels\Tapes\Brother;
use App\Helpers\Helper;
use App\Models\Labels\Label;
/*
* Rotated Label (print direction = landscape) for 62mm wide labels
*/
abstract class TZe_62mm_Landscape extends Label
{
private const WIDTH = 62.00;
private const MARGIN_SIDES = 1.50;
private const MARGIN_ENDS = 1.50;
public function getWidth() { return Helper::convertUnit(self::WIDTH, 'mm', $this->getUnit()); }
public function getMarginTop() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit()); }
public function getMarginBottom() { return Helper::convertUnit(self::MARGIN_SIDES, 'mm', $this->getUnit());}
public function getMarginLeft() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
public function getMarginRight() { return Helper::convertUnit(self::MARGIN_ENDS, 'mm', $this->getUnit()); }
public function getRotation() { return 90; }
}
@@ -1,106 +0,0 @@
<?php
namespace App\Models\Labels\Tapes\Brother;
class TZe_62mm_Landscape_A extends TZe_62mm_Landscape
{
public function getUnit() { return 'mm'; }
public function getHeight() { return 31.50; }
public function getSupportAssetTag() { return true; }
public function getSupport1DBarcode() { return true; }
public function getSupport2DBarcode() { return true; }
public function getSupportFields() { return 2; }
public function getSupportLogo() { return true; }
public function getSupportTitle() { return true; }
private const BARCODE1D_HEIGHT = 3.00;
private const BARCODE1D_MARGIN = 3.00;
private const BARCODE2D_SIZE = 20.00;
private const BARCODE2D_MARGIN = 1.50;
private const TAG_SIZE = 3.00;
private const LOGO_HEIGHT = 10.00;
private const LOGO_MARGIN = 1.50;
private const TITLE_SIZE = 3.00;
private const TITLE_MARGIN = 0.50;
private const LABEL_SIZE = 2.00;
private const LABEL_MARGIN = - 0.35;
private const FIELD_SIZE = 3.00;
private const FIELD_MARGIN = 0.10;
public function preparePDF($pdf) {}
public function write($pdf, $record) {
$pa = $this->getPrintableArea();
$currentX = $pa->x1;
$currentY = $pa->y1;
// Wide 1D barcode on top
if ($record->has('barcode1d')) {
static::write1DBarcode(
$pdf, $record->get('barcode1d')->content, $record->get('barcode1d')->type,
$currentX, $currentY, $pa->w, self::BARCODE1D_HEIGHT
);
$currentY = self::BARCODE1D_HEIGHT + self::BARCODE1D_MARGIN;
}
// Left column
if ($record->has('barcode2d')) {
$columnY = $currentY;
static::write2DBarcode(
$pdf, $record->get('barcode2d')->content, $record->get('barcode2d')->type,
$currentX, $columnY,
self::BARCODE2D_SIZE, self::BARCODE2D_SIZE
);
$columnY += self::BARCODE2D_SIZE + self::BARCODE2D_MARGIN;
static::writeText(
$pdf, $record->get('tag'),
$currentX, $columnY,
'freemono', 'b', self::TAG_SIZE, 'C',
self::BARCODE2D_SIZE, self::TAG_SIZE, true, 0
);
$currentX += self::BARCODE2D_SIZE + self::BARCODE2D_MARGIN;
}
// Right column
if ($record->get('logo')) {
static::writeImage(
$pdf, $record->get('logo'),
$currentX, $currentY,
$pa->w - $currentX, self::LOGO_HEIGHT,
'L', 'T', 300, true, false, 0
);
$currentY += self::LOGO_HEIGHT + self::LOGO_MARGIN;
}
if ($record->has('title')) {
static::writeText(
$pdf, $record->get('title'),
$currentX, $currentY,
'freesans', '', self::TITLE_SIZE, 'L',
$pa->w - $currentX, self::TITLE_SIZE, true, 0
);
$currentY += self::TITLE_SIZE + self::TITLE_MARGIN;
}
foreach ($record->get('fields') as $field) {
static::writeText(
$pdf, $field['label'],
$currentX, $currentY,
'freesans', '', self::LABEL_SIZE, 'L',
$pa->w - $currentX, self::LABEL_SIZE, true, 0, 0
);
$currentY += self::LABEL_SIZE + self::LABEL_MARGIN;
static::writeText(
$pdf, $field['value'],
$currentX, $currentY,
'freemono', 'B', self::FIELD_SIZE, 'L',
$pa->w - $currentX, self::FIELD_SIZE, true, 0, 0.3
);
$currentY += self::FIELD_SIZE + self::FIELD_MARGIN;
}
}
}
+150
View File
@@ -0,0 +1,150 @@
<?php
namespace App\Models\Traits;
use App\Models\CustomField;
use App\Models\CustomFieldset;
use Illuminate\Support\Collection;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Event;
use App\Models\DefaultValuesForCustomFields;
/*********************************
* Trait HasCustomFields
* @package App\Models\Traits
*
* How to use: declare a PHP function getFieldset that will return your fieldset (not the ID, the actual set)
*
*/
trait HasCustomFields
{
protected static function bootHasCustomFields()
{
// https://tech.chrishardie.com/2022/define-fire-listen-custom-laravel-model-events-trait/
static::registerModelEvent('validating', function ($model, $event) {
// \Log::error("Uh, something happened? Something good, maybe?");
// \Log::error("model: $model, event: $event");
// \Log::error("WHATS MY NAME? " . HasCustomFields::class);
// dump(class_uses_recursive($model));
if (in_array(HasCustomFields::class, class_uses_recursive($model))) {
\Log::error("!!!!!!!!!!!!! YOU ARE USING THE TRAIT!");
self::augmentValidationRulesForCustomFields($model);
} else {
\Log::error("You aren't useing the trait so go away");
}
});
}
/***************
* @return CustomFieldset|null
*
* This function by default will use the "getFieldsetKey()" method to
* return the customFieldset (or null) for this particular item. If
* necessary, you can override this method if your getFieldsetKey()
* cannot respond to `->fieldset` or `->id`.
*/
public function getFieldset(): ?CustomFieldset {
$pivot = $this->getFieldsetKey();
if(is_int($pivot)) { //why does this look just like the other thing? (below, look for is_int()
return CustomFieldset::find($pivot);
}
return $pivot?->fieldset; //this is bonkers, why is this even firing?!
}
/**********************
* @return Object|int|null
* (if this is in PHP 8.0, can we just put that as the signature?)
*
* This is the main method you have to override. It should either return an
* Object who you can call `->fieldset` on and get a fieldset object, and also
* be able to call `->id` on to get a unique key to be able to show custom fields.
* For example, for Assets, the element that is returned is the 'model' for the Asset.
* For something like Users, which will probably have only one universal set of custom fields,
* it should just return the Fieldset ID for it. Or, if there are no custom fields, it should
* return null
*/
abstract public function getFieldsetKey(): Object|int|null; // php v8 minimum, GOOD. TODO
/***********************
* @param int $fieldset_id
* @return Collection
*
* This is the main method you need to override to return a list of things that are *using* this fieldset
* The format is an array with keys: a URL, and values. So, for assets, it might return
* {
* "models/14" => "MacBook Pro 13 (model no: 12345)"
* }
*/
abstract public static function getFieldsetUsers(int $fieldset_id): array;
public static function augmentValidationRulesForCustomFields($model) {
\Log::debug("Augmenting validation rules for custom fields!!!!!!");
$fieldset = $model->getFieldset();
if ($fieldset) {
foreach ($fieldset->fields as $field){
if($field->format == 'BOOLEAN'){ // TODO - this 'feels' like entanglement of concerns?
$model->{$field->db_column} = filter_var($model->{$model->db_column}, FILTER_VALIDATE_BOOLEAN);
}
}
if(!$model->rules) {
$model->rules = [];
}
$model->rules += $model->getFieldset()->validation_rules();
\Log::debug("FINAL RULES ARE: ".print_r($model->rules,true));
}
}
public function getDefaultValue(CustomField $field)
{
$pivot = $this->getFieldsetKey(); // TODO - feels copypasta-ish?
$key_id = null;
if( is_int($pivot) ) { // TODO: *WHY* does this code repeat?!
$key_id = $pivot; // now we're done
} elseif( is_object($pivot) ) {
$key_id = $pivot?->id;
}
if(is_null($key_id)) {
return;
}
// TODO - begninng to think my custom scope really should be just an integer :/
return DefaultValuesForCustomFields::where('type',self::class)
->where('custom_field_id',$field->id)
->where('item_pivot_id',$key_id)->first()?->default_value;
}
public function customFill(Request $request, User $user, bool $shouldSetDefaults = false) {
$success = true;
if ($this->getFieldset()) {
foreach ($this->getFieldset()->fields as $field) {
if (is_array($request->input($field->db_column))) {
$field_value = implode(', ', $request->input($field->db_column));
} else {
$field_value = $request->input($field->db_column);
}
if ($shouldSetDefaults && (is_null($field_value) || $field_value === '')) {
$field_value = $this->getDefaultValue($field);
}
if ($field->field_encrypted == '1') {
if ($user->can('admin')) {
$this->{$field->db_column} = Crypt::encrypt($field_value);
} else {
$success = false;
continue; //may not be necessary? I'm not sure. I like the other way of doing this TODO
}
} else {
$this->{$field->db_column} = $request->input($field->db_column);
}
}
}
return $success;
}
}
+31 -46
View File
@@ -3,8 +3,10 @@
namespace App\Models;
use App\Http\Traits\UniqueUndeletedTrait;
use App\Models\Traits\HasCustomFields;
use App\Models\Traits\Searchable;
use App\Presenters\Presentable;
use Illuminate\Support\Facades\DB;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
@@ -23,7 +25,6 @@ use Watson\Validating\ValidatingTrait;
class User extends SnipeModel implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, HasLocalePreference
{
use HasFactory;
use CompanyableTrait;
protected $presenter = \App\Presenters\UserPresenter::class;
use SoftDeletes, ValidatingTrait;
@@ -32,6 +33,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
use Notifiable;
use Presentable;
use Searchable;
use HasCustomFields;
protected $hidden = ['password', 'remember_token', 'permissions', 'reset_password_code', 'persist_code'];
protected $table = 'users';
@@ -137,6 +139,20 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
'manager' => ['first_name', 'last_name', 'username'],
];
public function getFieldsetKey(): object|int|null
{
// TODO/FIXME - that's hardcoded text, but what language should you use?! I don't know.
// also TODO - is this going to beat on the DB too hard?
return CustomFieldset::where('type', User::class)->first()?->id;
}
public static function getFieldsetUsers(int $fieldset_id): array
{
return [
'no_idea_what_id_to_put' => 'No idea what string to put?' // FIXME obvs.
];
}
/**
* Internally check the user permission for the given section
*
@@ -204,23 +220,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $this->checkPermissionSection('superuser');
}
/**
* Checks if the can edit their own profile
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v6.3.4]
* @return bool
*/
public function canEditProfile() : bool {
$setting = Setting::getSettings();
if ($setting->profile_edit == 1) {
return true;
}
return false;
}
/**
* Checks if the user is deletable
*
@@ -481,6 +480,8 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
/**
* Establishes the user -> uploads relationship
*
* @todo I don't think we use this?
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v3.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
@@ -494,21 +495,6 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
->orderBy('created_at', 'desc');
}
/**
* Establishes the user -> acceptances relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v7.0.7]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function acceptances()
{
return $this->hasMany(\App\Models\Actionlog::class, 'target_id')
->where('target_type', self::class)
->where('action_type', '=', 'accepted')
->orderBy('created_at', 'desc');
}
/**
* Establishes the user -> requested assets relationship
*
@@ -602,6 +588,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
if ($format=='firstname.lastname') {
$username = str_slug($first_name) . '.' . str_slug($last_name);
} elseif ($format == 'lastnamefirstinitial') {
$username = str_slug($last_name.substr($first_name, 0, 1));
} elseif ($format == 'firstintial.lastname') {
@@ -618,9 +605,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
$username = str_slug($first_name).str_slug($last_name);
} elseif ($format == 'firstnamelastinitial') {
$username = str_slug(($first_name.substr($last_name, 0, 1)));
} elseif ($format == 'lastname.firstname') {
$username = str_slug($last_name).'.'.str_slug($first_name);
}
}
}
$user['first_name'] = $first_name;
@@ -858,16 +843,16 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $query->where('location_id','=', $location)
->where('users.first_name', 'LIKE', '%' . $search . '%')
->orWhere('users.email', 'LIKE', '%' . $search . '%')
->orWhere('users.last_name', 'LIKE', '%' . $search . '%')
->orWhere('users.permissions', 'LIKE', '%' . $search . '%')
->orWhere('users.country', 'LIKE', '%' . $search . '%')
->orWhere('users.phone', 'LIKE', '%' . $search . '%')
->orWhere('users.jobtitle', 'LIKE', '%' . $search . '%')
->orWhere('users.employee_num', 'LIKE', '%' . $search . '%')
->orWhere('users.username', 'LIKE', '%' . $search . '%')
->orwhereRaw('CONCAT(users.first_name," ",users.last_name) LIKE \''.$search.'%\'');
->where('first_name', 'LIKE', '%' . $search . '%')
->orWhere('email', 'LIKE', '%' . $search . '%')
->orWhere('last_name', 'LIKE', '%' . $search . '%')
->orWhere('permissions', 'LIKE', '%' . $search . '%')
->orWhere('country', 'LIKE', '%' . $search . '%')
->orWhere('phone', 'LIKE', '%' . $search . '%')
->orWhere('jobtitle', 'LIKE', '%' . $search . '%')
->orWhere('employee_num', 'LIKE', '%' . $search . '%')
->orWhere('username', 'LIKE', '%' . $search . '%')
->orwhereRaw('CONCAT(first_name," ",last_name) LIKE \''.$search.'%\'');
+1 -1
View File
@@ -92,7 +92,7 @@ class AssetObserver
// only modify the 'next' one if it's *bigger* than the stored base
//
if ($next_asset_tag > $settings->next_auto_tag_base && $next_asset_tag < PHP_INT_MAX) {
if($next_asset_tag > $settings->next_auto_tag_base) {
$settings->next_auto_tag_base = $next_asset_tag;
$settings->save();
}
+13 -14
View File
@@ -2,8 +2,11 @@
namespace App\Presenters;
use App\Helpers\CustomFieldHelper;
use App\Models\Asset;
use App\Models\CustomField;
use Carbon\CarbonImmutable;
use App\Models\CustomFieldset;
use DateTime;
/**
@@ -299,25 +302,21 @@ class AssetPresenter extends Presenter
// models. We only pass the fieldsets that pertain to each asset (via their model) so that we
// don't junk up the REST API with tons of custom fields that don't apply
$fields = CustomField::whereHas('fieldset', function ($query) {
$query->whereHas('models');
})->get();
//only get fieldsets that have fields
$fieldsets = CustomFieldset::where("type", Asset::class)->whereHas('fields')->get();
$ids = [];
foreach($fieldsets as $fieldset) {
if (count($fieldset->customizables()) > 0) { //only get fieldsets that are 'in use'
$ids[] = $fieldset->id;
}
}
$fields = CustomField::whereIn('id',$ids)->get(); // FIXME: d'oh! this is wrong. We just got fieldsets, above. Now we're getting fields?
// Note: We do not need to e() escape the field names here, as they are already escaped when
// they are presented in the blade view. If we escape them here, custom fields with quotes in their
// name can break the listings page. - snipe
foreach ($fields as $field) {
$layout[] = [
'field' => 'custom_fields.'.$field->db_column,
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => $field->name,
'formatter'=> 'customFieldsFormatter',
'escape' => true,
'class' => ($field->field_encrypted == '1') ? 'css-padlock' : '',
'visible' => ($field->show_in_listview == '1') ? true : false,
];
$layout[] = CustomFieldHelper::present($field);
}
$layout[] = [
+32 -16
View File
@@ -2,8 +2,14 @@
namespace App\Presenters;
use App\Helpers\CustomFieldHelper;
use App\Helpers\Helper;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\CustomFieldset;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
@@ -223,7 +229,6 @@ class UserPresenter extends Presenter
'class' => 'css-barcode',
'title' => trans('general.assets'),
'visible' => true,
'formatter' => 'linkNumberToUserAssetsFormatter',
],
[
'field' => 'licenses_count',
@@ -233,7 +238,6 @@ class UserPresenter extends Presenter
'class' => 'css-license',
'title' => trans('general.licenses'),
'visible' => true,
'formatter' => 'linkNumberToUserLicensesFormatter',
],
[
'field' => 'consumables_count',
@@ -243,7 +247,6 @@ class UserPresenter extends Presenter
'class' => 'css-consumable',
'title' => trans('general.consumables'),
'visible' => true,
'formatter' => 'linkNumberToUserConsumablesFormatter',
],
[
'field' => 'accessories_count',
@@ -253,7 +256,6 @@ class UserPresenter extends Presenter
'class' => 'css-accessory',
'title' => trans('general.accessories'),
'visible' => true,
'formatter' => 'linkNumberToUserAccessoriesFormatter',
],
[
'field' => 'manages_users_count',
@@ -263,7 +265,6 @@ class UserPresenter extends Presenter
'class' => 'css-users',
'title' => trans('admin/users/table.managed_users'),
'visible' => true,
'formatter' => 'linkNumberToUserManagedUsersFormatter',
],
[
'field' => 'manages_locations_count',
@@ -273,7 +274,6 @@ class UserPresenter extends Presenter
'class' => 'css-location',
'title' => trans('admin/users/table.managed_locations'),
'visible' => true,
'formatter' => 'linkNumberToUserManagedLocationsFormatter',
],
[
'field' => 'notes',
@@ -392,6 +392,30 @@ class UserPresenter extends Presenter
],
];
// TODO - FIXME - this is all copy-pasta'ed from the AssetPresenter! <start>
//only get fieldsets that have fields
$fieldsets = CustomFieldset::where("type", User::class)->whereHas('fields')->get();
$ids = [];
foreach($fieldsets as $fieldset) {
if (count($fieldset->customizables()) > 0) { //only get fieldsets that are 'in use'
\Log::debug("Found a fieldset! It's: ".$fieldset->id);
$ids[] = $fieldset->id;
} else {
\Log::debug("Didn't find fieldset: ".$fieldset->id);
}
}
$fields = CustomField::whereHas('fieldset', function (Builder $query) use($ids) {
$query->whereIn('custom_fieldsets.id', $ids);
})->get();
// Note: We do not need to e() escape the field names here, as they are already escaped when
// they are presented in the blade view. If we escape them here, custom fields with quotes in their
// name can break the listings page. - snipe
foreach ($fields as $field) {
\Log::debug("iterating through fields!");
$layout[] = CustomFieldHelper::present($field);
}
return json_encode($layout);
}
@@ -432,8 +456,6 @@ class UserPresenter extends Presenter
*/
public function gravatar()
{
// User's specific avatar
if ($this->avatar) {
// Check if it's a google avatar or some external avatar
@@ -445,12 +467,6 @@ class UserPresenter extends Presenter
return Storage::disk('public')->url('avatars/'.e($this->avatar));
}
// If there is a default avatar
if (Setting::getSettings()->default_avatar!= '') {
return Storage::disk('public')->url('avatars/'.e(Setting::getSettings()->default_avatar));
}
// Fall back to Gravatar if the settings allow loading remote scripts
if (Setting::getSettings()->load_remote == '1') {
if ($this->model->gravatar != '') {
@@ -464,8 +480,8 @@ class UserPresenter extends Presenter
}
}
return false;
// Set a fun, gender-neutral default icon
return config('app.url').'/img/default-sm.png';
}
/**
+8 -14
View File
@@ -21,7 +21,6 @@ use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\URL;
/**
* This service provider handles setting the observers on models
@@ -32,7 +31,7 @@ use Illuminate\Support\Facades\URL;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap application services.
* Custom email array validation
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
@@ -40,24 +39,19 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(UrlGenerator $url)
{
/**
* This is a workaround for proxies/reverse proxies that don't always pass the proper headers.
*
* Here, we check if the APP_URL starts with https://, which we should always honor,
* regardless of how well the proxy or network is configured.
*
* We'll force the https scheme if the APP_URL starts with https://, or if APP_FORCE_TLS is set to true.
*
*/
if ((strpos(env('APP_URL'), 'https://') === 0) || (env('APP_FORCE_TLS'))) {
$url->forceScheme('https');
if (env('APP_FORCE_TLS')) {
if (strpos(env('APP_URL'), 'https') === 0) {
$url->forceScheme('https');
} else {
Log::debug("'APP_FORCE_TLS' is set to true, but 'APP_URL' does not start with 'https://'. Will not force TLS on connections.");
}
}
// TODO - isn't it somehow 'gauche' to check the environment directly; shouldn't we be using config() somehow?
if ( ! env('APP_ALLOW_INSECURE_HOSTS')) { // unless you set APP_ALLOW_INSECURE_HOSTS, you should PROHIBIT forging domain parts of URL via Host: headers
$url_parts = parse_url(config('app.url'));
if ($url_parts && array_key_exists('scheme', $url_parts) && array_key_exists('host', $url_parts)) { // check for the *required* parts of a bare-minimum URL
URL::forceRootUrl(config('app.url'));
\URL::forceRootUrl(config('app.url'));
} else {
Log::error("Your APP_URL in your .env is misconfigured - it is: ".config('app.url').". Many things will work strangely unless you fix it.");
}
-8
View File
@@ -87,7 +87,6 @@ class AuthServiceProvider extends ServiceProvider
]);
$this->registerPolicies();
//Passport::routes(); //this is no longer required in newer passport versions
Passport::tokensExpireIn(Carbon::now()->addYears(config('passport.expiration_years')));
Passport::refreshTokensExpireIn(Carbon::now()->addYears(config('passport.expiration_years')));
Passport::personalAccessTokensExpireIn(Carbon::now()->addYears(config('passport.expiration_years')));
@@ -232,12 +231,5 @@ class AuthServiceProvider extends ServiceProvider
|| $user->can('update', User::class)
|| $user->can('create', User::class);
});
// This determines whether the user can edit their profile based on the setting in Admin > General
Gate::define('self.profile', function ($user) {
return $user->canEditProfile();
});
}
}
-1
View File
@@ -163,7 +163,6 @@ class Saml
OneLogin_Saml2_Utils::setProxyVars(request()->isFromTrustedProxy());
data_set($settings, 'sp.entityId', config('app.url'));
data_set($settings, 'baseurl', config('app.url') . '/saml');
data_set($settings, 'sp.assertionConsumerService.url', route('saml.acs'));
data_set($settings, 'sp.singleLogoutService.url', route('saml.sls'));
data_set($settings, 'sp.x509cert', $setting->saml_sp_x509cert);
-29
View File
@@ -2,7 +2,6 @@
namespace App\Services;
use App\Helpers\Helper;
use Illuminate\Translation\Translator;
/***************************************************************
@@ -18,10 +17,6 @@ use Illuminate\Translation\Translator;
***************************************************************/
class SnipeTranslator extends Translator {
static $legacy_translation_namespaces = [
"backup::" //Spatie backup uses 'legacy' locale names
];
//This is copied-and-pasted (almost) verbatim from Illuminate\Translation\Translator
public function choice($key, $number, array $replace = [], $locale = null)
{
@@ -44,28 +39,4 @@ class SnipeTranslator extends Translator {
);
}
public function get($key, array $replace = [], $locale = null, $fallback = true)
{
$modified_locale = $locale;
$changed_fallback = false;
$previous_fallback = $this->fallback;
// 'legacy' translation directories tend to be two-char ('en'), not 5-char ('en-US').
// Here we try our best to handle that.
foreach (self::$legacy_translation_namespaces as $namespace) {
if (preg_match("/^$namespace/", $key)) {
$modified_locale = Helper::mapBackToLegacyLocale($locale);
$changed_fallback = true;
$this->fallback = 'en'; //TODO - should this be 'env-able'? Or do we just put our foot down and say 'en'?
break;
}
}
$result = parent::get($key, $replace, $modified_locale, $fallback);
if ($changed_fallback) {
$this->fallback = $previous_fallback;
}
return $result;
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ class Label implements View
$pdf = new TCPDF(
$template->getOrientation(),
$template->getUnit(),
[0 => $template->getWidth(), 1 => $template->getHeight(), 'Rotate' => $template->getRotation()]
[ $template->getWidth(), $template->getHeight() ]
);
// Reset parameters
+8 -8
View File
@@ -26,7 +26,7 @@
"alek13/slack": "^2.0",
"arietimmerman/laravel-scim-server": "dev-master",
"bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-debugbar": "^3.13",
"barryvdh/laravel-debugbar": "^3.6",
"barryvdh/laravel-dompdf": "^2.0",
"doctrine/cache": "^1.10",
"doctrine/dbal": "^3.1",
@@ -49,7 +49,7 @@
"laravelcollective/html": "^6.2",
"league/csv": "^9.7",
"league/flysystem-aws-s3-v3": "^3.0",
"livewire/livewire": "^3.5",
"livewire/livewire": "^2.4",
"neitanod/forceutf8": "^2.0",
"nesbot/carbon": "^2.32",
"nunomaduro/collision": "^6.1",
@@ -61,7 +61,7 @@
"phpspec/prophecy": "^1.10",
"pragmarx/google2fa-laravel": "^1.3",
"rollbar/rollbar-laravel": "^8.0",
"spatie/laravel-backup": "^8.8",
"spatie/laravel-backup": "^8.0",
"spatie/laravel-ignition": "^2.0",
"tecnickcom/tc-lib-barcode": "^1.15",
"tecnickcom/tcpdf": "^6.5",
@@ -70,17 +70,17 @@
},
"suggest": {
"ext-ldap": "*",
"ext-zip": "*",
"ext-exif": "*"
"ext-zip": "*"
},
"require-dev": {
"brianium/paratest": "^v6.4.4",
"fakerphp/faker": "^1.16",
"larastan/larastan": "^2.9",
"mockery/mockery": "^1.4",
"nunomaduro/larastan": "^2.0",
"nunomaduro/phpinsights": "^2.7",
"php-mock/php-mock-phpunit": "^2.10",
"phpunit/phpunit": "^9.6.19",
"php-mock/php-mock-phpunit": "^2.8",
"phpunit/php-token-stream": "^3.1",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5",
"symfony/css-selector": "^4.4",
"symfony/dom-crawler": "^4.4",
Generated
+815 -1480
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -112,7 +112,7 @@ return [
|
*/
'fallback_locale' => env('FALLBACK_APP_LOCALE', 'en-US'),
'fallback_locale' => 'en-US',
/*
|--------------------------------------------------------------------------
+6 -25
View File
@@ -121,12 +121,12 @@ return [
'notifications' => [
'notifications' => [
\Spatie\Backup\Notifications\Notifications\BackupHasFailedNotification::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFoundNotification::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\CleanupHasFailedNotification::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\BackupWasSuccessfulNotification::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\HealthyBackupWasFoundNotification::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\CleanupWasSuccessfulNotification::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\BackupHasFailed::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFound::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\CleanupHasFailed::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\BackupWasSuccessful::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\HealthyBackupWasFound::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
\Spatie\Backup\Notifications\Notifications\CleanupWasSuccessful::class => [env('MAIL_BACKUP_NOTIFICATION_DRIVER', null)],
],
/*
@@ -137,11 +137,6 @@ return [
'mail' => [
'to' => env('MAIL_BACKUP_NOTIFICATION_ADDRESS', null),
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
],
'slack' => [
@@ -157,20 +152,6 @@ return [
'icon' => null,
],
'discord' => [
'webhook_url' => '',
/*
* If this is an empty string, the name field on the webhook will be used.
*/
'username' => '',
/*
* If this is an empty string, the avatar on the webhook will be used.
*/
'avatar_url' => '',
],
],
/*
+106 -108
View File
@@ -3,158 +3,156 @@
return [
/*
|---------------------------------------------------------------------------
|--------------------------------------------------------------------------
| Class Namespace
|---------------------------------------------------------------------------
|--------------------------------------------------------------------------
|
| This value sets the root class namespace for Livewire component classes in
| your application. This value will change where component auto-discovery
| finds components. It's also referenced by the file creation commands.
| This value sets the root namespace for Livewire component classes in
| your application. This value affects component auto-discovery and
| any Livewire file helper commands, like `artisan make:livewire`.
|
| After changing this item, run: `php artisan livewire:discover`.
|
*/
'class_namespace' => 'App\\Livewire',
'class_namespace' => 'App\\Http\\Livewire',
/*
|---------------------------------------------------------------------------
|--------------------------------------------------------------------------
| View Path
|---------------------------------------------------------------------------
|--------------------------------------------------------------------------
|
| This value is used to specify where Livewire component Blade templates are
| stored when running file creation commands like `artisan make:livewire`.
| It is also used if you choose to omit a component's render() method.
| This value sets the path for Livewire component views. This affects
| file manipulation helper commands like `artisan make:livewire`.
|
*/
'view_path' => resource_path('views/livewire'),
/*
|---------------------------------------------------------------------------
|--------------------------------------------------------------------------
| Layout
|---------------------------------------------------------------------------
| The view that will be used as the layout when rendering a single component
| as an entire page via `Route::get('/post/create', CreatePost::class);`.
| In this case, the view returned by CreatePost will render into $slot.
|--------------------------------------------------------------------------
| The default layout view that will be used when rendering a component via
| Route::get('/some-endpoint', SomeComponent::class);. In this case the
| the view returned by SomeComponent will be wrapped in "layouts.app"
|
*/
'layout' => 'components.layouts.app',
'layout' => 'layouts.app',
/*
|---------------------------------------------------------------------------
| Lazy Loading Placeholder
|---------------------------------------------------------------------------
| Livewire allows you to lazy load components that would otherwise slow down
| the initial page load. Every component can have a custom placeholder or
| you can define the default placeholder view for all components below.
|--------------------------------------------------------------------------
| Livewire Assets URL
|--------------------------------------------------------------------------
|
| This value sets the path to Livewire JavaScript assets, for cases where
| your app's domain root is not the correct path. By default, Livewire
| will load its JavaScript assets from the app's "relative root".
|
| Examples: "/assets", "myurl.com/app".
|
*/
'lazy_placeholder' => null,
'asset_url' => env('APP_URL'),
/*
|---------------------------------------------------------------------------
| Temporary File Uploads
|---------------------------------------------------------------------------
|--------------------------------------------------------------------------
| Livewire App URL
|--------------------------------------------------------------------------
|
| This value should be used if livewire assets are served from CDN.
| Livewire will communicate with an app through this url.
|
| Examples: "https://my-app.com", "myurl.com/app".
|
*/
'app_url' => null,
/*
|--------------------------------------------------------------------------
| Livewire Endpoint Middleware Group
|--------------------------------------------------------------------------
|
| This value sets the middleware group that will be applied to the main
| Livewire "message" endpoint (the endpoint that gets hit everytime
| a Livewire component updates). It is set to "web" by default.
|
*/
'middleware_group' => 'web',
/*
|--------------------------------------------------------------------------
| Livewire Temporary File Uploads Endpoint Configuration
|--------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is stored permanently. All file uploads are directed to
| a global endpoint for temporary storage. You may configure this below:
| before the file is validated and stored permanently. All file uploads
| are directed to a global endpoint for temporary storage. The config
| items below are used for customizing the way the endpoint works.
|
*/
'temporary_file_upload' => [
'disk' => env('PRIVATE_FILESYSTEM_DISK', 'local'), // Example: 'local', 's3' Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
'rules' => null, // Example: ['file', 'mimes:png,jpg'] Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' Default 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs.
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
'max_upload_time' => 5, // Max duration (in minutes) before an upload gets invalidated.
],
/*
|---------------------------------------------------------------------------
| Render On Redirect
|---------------------------------------------------------------------------
|--------------------------------------------------------------------------
| Manifest File Path
|--------------------------------------------------------------------------
|
| This value determines if Livewire will run a component's `render()` method
| after a redirect has been triggered using something like `redirect(...)`
| Setting this to true will render the view once more before redirecting
| This value sets the path to the Livewire manifest file.
| The default should work for most cases (which is
| "<app_root>/bootstrap/cache/livewire-components.php"), but for specific
| cases like when hosting on Laravel Vapor, it could be set to a different value.
|
| Example: for Laravel Vapor, it would be "/tmp/storage/bootstrap/cache/livewire-components.php".
|
*/
'manifest_path' => null,
/*
|--------------------------------------------------------------------------
| Back Button Cache
|--------------------------------------------------------------------------
|
| This value determines whether the back button cache will be used on pages
| that contain Livewire. By disabling back button cache, it ensures that
| the back button shows the correct state of components, instead of
| potentially stale, cached data.
|
| Setting it to "false" (default) will disable back button cache.
|
*/
'back_button_cache' => false,
/*
|--------------------------------------------------------------------------
| Render On Redirect
|--------------------------------------------------------------------------
|
| This value determines whether Livewire will render before it's redirected
| or not. Setting it to "false" (default) will mean the render method is
| skipped when redirecting. And "true" will mean the render method is
| run before redirecting. Browsers bfcache can store a potentially
| stale view if render is skipped on redirect.
|
*/
'render_on_redirect' => false,
/*
|---------------------------------------------------------------------------
| Eloquent Model Binding
|---------------------------------------------------------------------------
|
| Previous versions of Livewire supported binding directly to eloquent model
| properties using wire:model by default. However, this behavior has been
| deemed too "magical" and has therefore been put under a feature flag.
|
*/
'legacy_model_binding' => true,
/*
|---------------------------------------------------------------------------
| Auto-inject Frontend Assets
|---------------------------------------------------------------------------
|
| By default, Livewire automatically injects its JavaScript and CSS into the
| <head> and <body> of pages containing Livewire components. By disabling
| this behavior, you need to use @livewireStyles and @livewireScripts.
|
*/
'inject_assets' => true,
/*
|---------------------------------------------------------------------------
| Navigate (SPA mode)
|---------------------------------------------------------------------------
|
| By adding `wire:navigate` to links in your Livewire application, Livewire
| will prevent the default link handling and instead request those pages
| via AJAX, creating an SPA-like effect. Configure this behavior here.
|
*/
'navigate' => [
'show_progress_bar' => true,
'progress_bar_color' => '#2299dd',
],
/*
|---------------------------------------------------------------------------
| HTML Morph Markers
|---------------------------------------------------------------------------
|
| Livewire intelligently "morphs" existing HTML into the newly rendered HTML
| after each update. To make this process more reliable, Livewire injects
| "markers" into the rendered Blade surrounding @if, @class & @foreach.
|
*/
'inject_morph_markers' => true,
/*
|---------------------------------------------------------------------------
| Pagination Theme
|---------------------------------------------------------------------------
|
| When enabling Livewire's pagination feature by using the `WithPagination`
| trait, Livewire will use Tailwind templates to render pagination views
| on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
|
*/
'pagination_theme' => 'tailwind',
];
+7 -2
View File
@@ -1,5 +1,7 @@
<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| DO NOT EDIT THIS FILE DIRECTLY.
@@ -57,6 +59,9 @@ return [
*
* @link https://symfony.com/doc/current/deployment/proxies.html
*/
// 'headers' => Illuminate\Http\Request::HEADER_X_FORWARDED_ALL, //this is mostly handled already
'headers' => Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB,
];
+5 -5
View File
@@ -1,10 +1,10 @@
<?php
return array (
'app_version' => 'v7.0.7',
'full_app_version' => 'v7.0.7 - build 14172-ge50296870',
'build_version' => '14172',
'app_version' => 'v7.0.0-dev',
'full_app_version' => 'v7.0.0 - build 8df9007-gb489c71fa2',
'build_version' => '13487',
'prerelease_version' => '',
'hash_version' => 'ge50296870',
'full_hash' => 'v7.0.7-53-ge50296870',
'hash_version' => '8df9007',
'full_hash' => 'v7.0.0-99-8df9007',
'branch' => 'develop',
);
-60
View File
@@ -105,64 +105,4 @@ class ActionlogFactory extends Factory
];
});
}
public function filesUploaded()
{
return $this->state(function () {
return [
'created_at' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get()),
'action_type' => 'uploaded',
'item_type' => User::class,
'filename' => $this->faker->unixTime('now'),
];
});
}
public function acceptedSignature()
{
return $this->state(function () {
$asset = Asset::factory()->create();
return [
'created_at' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get()),
'action_type' => 'accepted',
'item_id' => $asset->id,
'item_type' => Asset::class,
'target_type' => User::class,
'accept_signature' => $this->faker->unixTime('now'),
];
});
}
public function acceptedEula()
{
return $this->state(function () {
$asset = Asset::factory()->create();
return [
'created_at' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get()),
'action_type' => 'accepted',
'item_id' => $asset->id,
'item_type' => Asset::class,
'target_type' => User::class,
'filename' => $this->faker->unixTime('now'),
];
});
}
public function userUpdated()
{
return $this->state(function () {
return [
'created_at' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get()),
'action_type' => 'update',
'target_type' => User::class,
'item_type' => User::class,
];
});
}
}
+11
View File
@@ -387,4 +387,15 @@ class AssetFactory extends Factory
$asset->setValidating(true);
});
}
public function withComplicatedCustomFields()
{
return $this->state(function () {
return [
'model_id' => function () {
return AssetModel::where('name', 'complicated')->first() ?? AssetModel::factory()->complicated();
}
];
});
}
}
+9
View File
@@ -448,4 +448,13 @@ class AssetModelFactory extends Factory
];
});
}
public function complicated()
{
return $this->state(function () {
return [
'name' => 'Complicated fieldset'
];
})->for(CustomFieldSet::factory()->complicated(), 'fieldset');
}
}
-13
View File
@@ -7,7 +7,6 @@ use App\Models\Company;
use App\Models\Consumable;
use App\Models\Manufacturer;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\Supplier;
@@ -117,16 +116,4 @@ class ConsumableFactory extends Factory
$consumable->category->update(['require_acceptance' => 1]);
});
}
public function checkedOutToUser(User $user = null)
{
return $this->afterCreating(function (Consumable $consumable) use ($user) {
$consumable->users()->attach($consumable->id, [
'consumable_id' => $consumable->id,
'created_at' => Carbon::now(),
'user_id' => User::factory()->create()->id,
'assigned_to' => $user->id ?? User::factory()->create()->id,
]);
});
}
}
+20 -1
View File
@@ -27,6 +27,7 @@ class CustomFieldFactory extends Factory
'element' => 'text',
'auto_add_to_fieldsets' => '0',
'show_in_requestable_list' => '0',
'type' => 'App\\Models\\Asset'
];
}
@@ -76,7 +77,7 @@ class CustomFieldFactory extends Factory
{
return $this->state(function () {
return [
'name' => 'MAC Address',
'name' => 'MAC Address EXPLICIT',
'format' => 'regex:/^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/',
];
});
@@ -93,6 +94,15 @@ class CustomFieldFactory extends Factory
});
}
public function plainText()
{
return $this->state(function () {
return [
'name' => 'plain_text',
];
});
}
public function testCheckbox()
{
return $this->state(function () {
@@ -117,4 +127,13 @@ class CustomFieldFactory extends Factory
});
}
public function date()
{
return $this->state(function () {
return [
'name' => 'date',
'format' => 'date'
];
});
}
}
+19 -7
View File
@@ -2,8 +2,8 @@
namespace Database\Factories;
use App\Models\CustomFieldset;
use App\Models\CustomField;
use App\Models\CustomFieldset;
use Illuminate\Database\Eloquent\Factories\Factory;
class CustomFieldsetFactory extends Factory
@@ -58,13 +58,13 @@ class CustomFieldsetFactory extends Factory
{
return $this->afterCreating(function (CustomFieldset $fieldset) use ($fields) {
if (empty($fields)) {
$mac_address = CustomField::factory()->macAddress()->create();
$ram = CustomField::factory()->ram()->create();
$cpu = CustomField::factory()->cpu()->create();
$mac_address = CustomField::factory()->macAddress()->create();
$ram = CustomField::factory()->ram()->create();
$cpu = CustomField::factory()->cpu()->create();
$fieldset->fields()->attach($mac_address, ['order' => '1', 'required' => false]);
$fieldset->fields()->attach($ram, ['order' => '2', 'required' => false]);
$fieldset->fields()->attach($cpu, ['order' => '3', 'required' => false]);
$fieldset->fields()->attach($mac_address, ['order' => '1', 'required' => false]);
$fieldset->fields()->attach($ram, ['order' => '2', 'required' => false]);
$fieldset->fields()->attach($cpu, ['order' => '3', 'required' => false]);
} else {
foreach ($fields as $field) {
$fieldset->fields()->attach($field, ['order' => '1', 'required' => false]);
@@ -72,4 +72,16 @@ class CustomFieldsetFactory extends Factory
}
});
}
public function complicated()
{
//$mac = CustomField::factory()->macAddress()->create();
return $this->state(function () {
return [
'name' => 'complicated'
];
})->hasAttached(CustomField::factory()->macAddress(), ['required' => false, 'order' => 0], 'fields')
->hasAttached(CustomField::factory()->plainText(), ['required' => true, 'order' => 1], 'fields')
->hasAttached(CustomField::factory()->date(), ['required' => false, 'order' => 2], 'fields');
}
}
-10
View File
@@ -38,16 +38,6 @@ class UserFactory extends Factory
];
}
public function deletedUser()
{
return $this->state(function () {
return [
'deleted_at' => $this->faker->dateTime(),
];
});
}
public function firstAdmin()
{
return $this->state(function () {
@@ -23,8 +23,8 @@ function updateLegacyColumnName($customfield)
$name_to_db_name = CustomField::name_to_db_name($customfield->name);
//\Log::debug('Trying to rename '.$name_to_db_name." to ".$customfield->convertUnicodeDbSlug()."...\n");
if (Schema::hasColumn(CustomField::$table_name, $name_to_db_name)) {
return Schema::table(CustomField::$table_name,
if (Schema::hasColumn('assets', $name_to_db_name)) {
return Schema::table('assets',
function ($table) use ($name_to_db_name, $customfield) {
$table->renameColumn($name_to_db_name, $customfield->convertUnicodeDbSlug());
}
@@ -81,8 +81,8 @@ class FixUtf8CustomFieldColumnNames extends Migration
// "_snipeit_imei_1" becomes "_snipeit_imei"
$legacyColumnName = (string) Str::of($currentColumnName)->replaceMatches('/_(\d)+$/', '');
if (Schema::hasColumn(CustomField::$table_name, $currentColumnName)) {
Schema::table(CustomField::$table_name, function (Blueprint $table) use ($currentColumnName, $legacyColumnName) {
if (Schema::hasColumn('assets', $currentColumnName)) {
Schema::table('assets', function (Blueprint $table) use ($currentColumnName, $legacyColumnName) {
$table->renameColumn(
$currentColumnName,
$legacyColumnName
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Models\CustomField;
use App\Models\Asset;
class AddTypeToCustomFields extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('custom_fields', function (Blueprint $table) {
//
$table->text('type')->default('App\\Models\\Asset'); // TODO this default is needed for a not-nullable column, I guess? I don't like this because it will silently 'fix' errors we should properly 'fix'
});
CustomField::query()->update(['type' => Asset::class]); // TODO - is this still necessary with that 'default'?
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('custom_fields', function (Blueprint $table) {
//
$table->dropColumn('type');
});
}
}
@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Models\CustomFieldset;
use App\Models\Asset;
class AddTypeToCustomFieldsets extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('custom_fieldsets', function (Blueprint $table) {
//
$table->text('type')->default('App\\Models\\Asset');
});
CustomFieldset::query()->update(['type' => Asset::class]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('custom_fieldsets', function (Blueprint $table) {
//
$table->dropColumn('type');
});
}
}
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class GeneralizeDefaultValuesForCustomFields extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::rename('models_custom_fields', 'default_values_for_custom_fields');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::rename('default_values_for_custom_fields', 'models_custom_fields');
}
}
@@ -0,0 +1,44 @@
<?php
use App\Models\Asset;
use App\Models\DefaultValuesForCustomFields;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddTypeColumnToDefaultValuesForCustomFields extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('default_values_for_custom_fields', function (Blueprint $table) {
$table->renameColumn('asset_model_id','item_pivot_id'); //this one works. okay. that's someting.
});
Schema::table('default_values_for_custom_fields', function (Blueprint $table) {
$table->string('type')->nullable();
});
DefaultValuesForCustomFields::query()->update(['type' => Asset::class]);
Schema::table('default_values_for_custom_fields', function (Blueprint $table) {
//$table->renameColumn('asset_model_id','item_pivot_id'); //this one works. okay. that's someting.
$table->string('type')->nullable(false)->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('default_values_for_custom_fields', function (Blueprint $table) {
$table->dropColumn('type');
$table->renameColumn('item_pivot_id','asset_model_id');
});
}
}
@@ -1,30 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('settings', function (Blueprint $table) {
$table->boolean('profile_edit')->nullable()->default(1);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('settings', function (Blueprint $table) {
if (Schema::hasColumn('settings', 'profile_edit')) {
$table->dropColumn('profile_edit');
}
});
}
};
@@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('settings', function (Blueprint $table) {
$table->string('default_avatar')->after('favicon')->default('default.png')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('settings', function (Blueprint $table) {
$table->dropColumn('default_avatar');
});
}
};
-50
View File
@@ -1,50 +0,0 @@
version: '3'
services:
snipeit:
build:
context: .
dockerfile: Dockerfile.alpine
container_name: snipeit
ports:
- "8000:80"
volumes:
- ./storage/logs:/var/www/html/storage/logs
depends_on:
- mariadb
- redis
env_file:
- .env.docker
networks:
- snipeit-backend
mariadb:
image: mariadb:10.6.4-focal
volumes:
- db:/var/lib/mysql
env_file:
- .env.docker
networks:
- snipeit-backend
ports:
- "3306:3306"
redis:
image: redis:6.2.5-buster
networks:
- snipeit-backend
mailhog:
image: mailhog/mailhog:v1.0.1
ports:
# - 1025:1025
- "8025:8025"
networks:
- snipeit-backend
volumes:
db: {}
networks:
snipeit-backend: {}
+46 -30
View File
@@ -1,34 +1,50 @@
volumes:
db_data:
storage:
version: '3'
services:
app:
image: snipe/snipe-it:${APP_VERSION:-v6.4.1}
restart: always
volumes:
- storage:/var/lib/snipeit
snipeit:
build:
context: .
dockerfile: Dockerfile.alpine
container_name: snipeit
ports:
- "${APP_PORT:-8000}:80"
depends_on:
db:
condition: service_healthy
restart: true
env_file:
- .env
db:
image: mariadb:10.6.4-focal
restart: always
- "8000:80"
volumes:
- db_data:/var/lib/mysql
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
healthcheck:
test: mysqladmin ping -h 127.0.0.1 -u $$MYSQL_USER --password=$$MYSQL_PASSWORD
interval: 5s
timeout: 1s
retries: 5
- ./storage/logs:/var/www/html/storage/logs
depends_on:
- mariadb
- redis
env_file:
- .env.docker
networks:
- snipeit-backend
mariadb:
image: mariadb:10.6.4-focal
volumes:
- db:/var/lib/mysql
env_file:
- .env.docker
networks:
- snipeit-backend
ports:
- "3306:3306"
redis:
image: redis:6.2.5-buster
networks:
- snipeit-backend
mailhog:
image: mailhog/mailhog:v1.0.1
ports:
# - 1025:1025
- "8025:8025"
networks:
- snipeit-backend
volumes:
db: {}
networks:
snipeit-backend: {}
+32 -23
View File
@@ -10,6 +10,7 @@
"acorn-import-assertions": "^1.9.0",
"admin-lte": "^2.4.18",
"ajv": "^6.12.6",
"alpinejs": "^3.13.10",
"blueimp-file-upload": "^9.34.0",
"bootstrap": "^3.4.1",
"bootstrap-colorpicker": "^2.5.3",
@@ -30,13 +31,13 @@
"less-loader": "^6.0",
"list.js": "^1.5.0",
"morris.js": "github:morrisjs/morris.js",
"papaparse": "5.4.1",
"papaparse": "5.2.0",
"select2": "4.0.13",
"sheetjs": "^2.0.0",
"signature_pad": "^4.2.0",
"tableexport.jquery.plugin": "1.30.0",
"tether": "^1.4.0",
"webpack": "^5.92.0"
"webpack": "^5.90.2"
},
"devDependencies": {
"all-contributors-cli": "^6.26.1",
@@ -2311,6 +2312,17 @@
"@types/node": "*"
}
},
"node_modules/@vue/reactivity": {
"version": "3.1.5",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.1.5"
}
},
"node_modules/@vue/shared": {
"version": "3.1.5",
"license": "MIT"
},
"node_modules/@webassemblyjs/ast": {
"version": "1.12.1",
"license": "MIT",
@@ -2497,14 +2509,6 @@
"acorn": "^8"
}
},
"node_modules/acorn-import-attributes": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
"integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
"peerDependencies": {
"acorn": "^8"
}
},
"node_modules/acorn-node": {
"version": "1.8.2",
"license": "Apache-2.0",
@@ -2658,6 +2662,14 @@
"prettier": "^2"
}
},
"node_modules/alpinejs": {
"version": "3.13.10",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.10.tgz",
"integrity": "sha512-86RB307VWICex0vG15Eq0x058cNNsvS57ohrjN6n/TJAVSFV+zXOK/E34nNHDHc6Poq+yTNCLqEzPqEkRBTMRQ==",
"dependencies": {
"@vue/reactivity": "~3.1.1"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"dev": true,
@@ -5299,9 +5311,8 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.17.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz",
"integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==",
"version": "5.16.0",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -8336,9 +8347,9 @@
"license": "(MIT AND Zlib)"
},
"node_modules/papaparse": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.2.0.tgz",
"integrity": "sha512-ylq1wgUSnagU+MKQtNeVqrPhZuMYBvOSL00DHycFTCxownF95gpLAk1HiHdUW77N8yxRq1qHXLdlIPyBSG9NSA=="
},
"node_modules/param-case": {
"version": "3.0.4",
@@ -10403,8 +10414,7 @@
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -10867,9 +10877,8 @@
"license": "BSD-2-Clause"
},
"node_modules/webpack": {
"version": "5.92.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz",
"integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==",
"version": "5.91.0",
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.5",
@@ -10877,10 +10886,10 @@
"@webassemblyjs/wasm-edit": "^1.12.1",
"@webassemblyjs/wasm-parser": "^1.12.1",
"acorn": "^8.7.1",
"acorn-import-attributes": "^1.9.5",
"acorn-import-assertions": "^1.9.0",
"browserslist": "^4.21.10",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.0",
"enhanced-resolve": "^5.16.0",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
+3 -2
View File
@@ -30,6 +30,7 @@
"acorn-import-assertions": "^1.9.0",
"admin-lte": "^2.4.18",
"ajv": "^6.12.6",
"alpinejs": "^3.13.10",
"blueimp-file-upload": "^9.34.0",
"bootstrap": "^3.4.1",
"bootstrap-colorpicker": "^2.5.3",
@@ -50,12 +51,12 @@
"less-loader": "^6.0",
"list.js": "^1.5.0",
"morris.js": "github:morrisjs/morris.js",
"papaparse": "5.4.1",
"papaparse": "5.2.0",
"select2": "4.0.13",
"sheetjs": "^2.0.0",
"signature_pad": "^4.2.0",
"tableexport.jquery.plugin": "1.30.0",
"tether": "^1.4.0",
"webpack": "^5.92.0"
"webpack": "^5.90.2"
}
}
-3
View File
@@ -554,7 +554,4 @@ div.container.row-new-striped {
.table > tfoot > tr.danger > th {
background-color: var(--back-sub);
}
.table > tbody > tr.warning > td {
background-color: var(--back-sub);
}
-3
View File
@@ -554,7 +554,4 @@ div.container.row-new-striped {
.table > tfoot > tr.danger > th {
background-color: var(--back-sub);
}
.table > tbody > tr.warning > td {
background-color: var(--back-sub);
}
-3
View File
@@ -525,7 +525,4 @@ a:visited {
.search-highlight:hover {
background-color: #e9d15b;
}
.table > tbody > tr.warning > td {
background-color: var(--back-sub);
}
-3
View File
@@ -525,7 +525,4 @@ a:visited {
.search-highlight:hover {
background-color: #e9d15b;
}
.table > tbody > tr.warning > td {
background-color: var(--back-sub);
}
-3
View File
@@ -512,7 +512,4 @@ a:visited {
.search-highlight:hover {
background-color: #e9d15b;
}
.table > tbody > tr.warning > td {
background-color: var(--back-sub);
}

Some files were not shown because too many files have changed in this diff Show More