Merge branch 'develop' into fixes/custom-field-values

# Conflicts:
#	resources/views/livewire/custom-field-set-default-values-for-model.blade.php
This commit is contained in:
Marcus Moore 2024-09-09 16:07:37 -07:00
commit 6423df2133
No known key found for this signature in database
1072 changed files with 7129 additions and 3218 deletions

View file

@ -3181,6 +3181,42 @@
"contributions": [
"code"
]
},
{
"login": "Glukose1",
"name": "Glukose1",
"avatar_url": "https://avatars.githubusercontent.com/u/167117705?v=4",
"profile": "https://github.com/Glukose1",
"contributions": [
"code"
]
},
{
"login": "Scarzy",
"name": "Scarzy",
"avatar_url": "https://avatars.githubusercontent.com/u/1197791?v=4",
"profile": "https://github.com/Scarzy",
"contributions": [
"code"
]
},
{
"login": "setpill",
"name": "setpill",
"avatar_url": "https://avatars.githubusercontent.com/u/37372069?v=4",
"profile": "https://github.com/setpill",
"contributions": [
"code"
]
},
{
"login": "swift2512",
"name": "swift2512",
"avatar_url": "https://avatars.githubusercontent.com/u/3755203?v=4",
"profile": "https://github.com/swift2512",
"contributions": [
"bug"
]
}
]
}

View file

@ -1,6 +1,8 @@
# --------------------------------------------
# REQUIRED: DB SETUP
# --------------------------------------------
# https://mariadb.com/kb/en/mariadb-server-docker-official-image-environment-variables/
MYSQL_DATABASE=snipeit
MYSQL_USER=snipeit
MYSQL_PASSWORD=changeme1234

View file

@ -32,6 +32,8 @@ DB_PREFIX=null
DB_DUMP_PATH='/usr/bin'
DB_CHARSET=utf8mb4
DB_COLLATION=utf8mb4_unicode_ci
DB_SANITIZE_BY_DEFAULT=false
# --------------------------------------------
# OPTIONAL: SSL DATABASE SETTINGS

43
.github/stale.yml vendored
View file

@ -1,43 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- :woman_technologist: ready for dev
- :moneybag: bounty
- :hand: bug
- "🔐 security"
- "👩‍💻 ready for dev"
- "💰 bounty"
- "✋ bug"
exemptMilestones: true
# Label to use when marking an issue as stale
staleLabel: stale
only: issues
# Comment to post when removing the stale label.
unmarkComment: >
Okay, it looks like this issue or feature request might still be important. We'll re-open
it for now. Thank you for letting us know!
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
Is this still relevant? We haven't heard from anyone in a bit. If so,
please comment with any updates or additional detail.
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Don't
take it personally, we just need to keep a handle on things. Thank you
for your contributions!
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue has been automatically closed because it has not had
recent activity. If you believe this is still an issue, please confirm that
this issue is still happening in the most recent version of Snipe-IT and reply
to this thread to re-open it.

39
.github/workflows/stale.yml vendored Normal file
View file

@ -0,0 +1,39 @@
name: 'Close stale issues'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
# contents: write # only for delete-branch option
issues: write
# pull-requests: write
steps:
- uses: actions/stale@v9
with:
debug-only: true
operations-per-run: 100 # just while we're debugging
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 60
days-before-close: 7
exempt-all-milestones: true
stale-issue-message: >
Is this still relevant? We haven't heard from anyone in a bit. If so,
please comment with any updates or additional detail.
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Don't
take it personally, we just need to keep a handle on things. Thank you
for your contributions!
close-issue-message: >
This issue has been automatically closed because it has not had
recent activity. If you believe this is still an issue, please confirm that
this issue is still happening in the most recent version of Snipe-IT and reply
to this thread to re-open it.
# There doesn't seem to be a 'reopen issue message'?
# Since there is no 'stale-pr-message' - PR's should not be stale'd
stale-issue-label: stale
exempt-issue-labels: >
pinned,security,:woman_technologist: ready for dev,:moneybag: bounty,:hand: bug,🔐 security,👩‍💻 ready for dev,💰 bounty,✋ bug

1
.gitignore vendored
View file

@ -47,6 +47,7 @@ storage/private_uploads/users/*
tests/_data/scenarios
tests/_output/*
tests/_support/_generated/*
tests/coverage/*
/npm-debug.log
/storage/oauth-private.key
/storage/oauth-public.key

View file

@ -52,6 +52,7 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/47315739?v=4" width="110px;"/><br /><sub>bilias</sub>](https://github.com/bilias)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bilias "Code") | [<img src="https://avatars.githubusercontent.com/u/2565989?v=4" width="110px;"/><br /><sub>coach1988</sub>](https://github.com/coach1988)<br />[💻](https://github.com/snipe/snipe-it/commits?author=coach1988 "Code") | [<img src="https://avatars.githubusercontent.com/u/11910225?v=4" width="110px;"/><br /><sub>MrM</sub>](https://github.com/mauro-miatello)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mauro-miatello "Code") | [<img src="https://avatars.githubusercontent.com/u/60405354?v=4" width="110px;"/><br /><sub>koiakoia</sub>](https://github.com/koiakoia)<br />[💻](https://github.com/snipe/snipe-it/commits?author=koiakoia "Code") | [<img src="https://avatars.githubusercontent.com/u/5323832?v=4" width="110px;"/><br /><sub>Mustafa Online</sub>](https://github.com/mustafa-online)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mustafa-online "Code") | [<img src="https://avatars.githubusercontent.com/u/104601439?v=4" width="110px;"/><br /><sub>franceslui</sub>](https://github.com/franceslui)<br />[💻](https://github.com/snipe/snipe-it/commits?author=franceslui "Code") | [<img src="https://avatars.githubusercontent.com/u/125313163?v=4" width="110px;"/><br /><sub>Q4kK</sub>](https://github.com/Q4kK)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Q4kK "Code") |
| [<img src="https://avatars.githubusercontent.com/u/55590532?v=4" width="110px;"/><br /><sub>squintfox</sub>](https://github.com/squintfox)<br />[💻](https://github.com/snipe/snipe-it/commits?author=squintfox "Code") | [<img src="https://avatars.githubusercontent.com/u/1380084?v=4" width="110px;"/><br /><sub>Jeff Clay</sub>](https://github.com/jeffclay)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jeffclay "Code") | [<img src="https://avatars.githubusercontent.com/u/52716446?v=4" width="110px;"/><br /><sub>Phil J R</sub>](https://github.com/PP-JN-RL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PP-JN-RL "Code") | [<img src="https://avatars.githubusercontent.com/u/1496725?v=4" width="110px;"/><br /><sub>i_virus</sub>](https://www.corelight.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=chandanchowdhury "Code") | [<img src="https://avatars.githubusercontent.com/u/1020541?v=4" width="110px;"/><br /><sub>Paul Grime</sub>](https://github.com/gitgrimbo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=gitgrimbo "Code") | [<img src="https://avatars.githubusercontent.com/u/922815?v=4" width="110px;"/><br /><sub>Lee Porte</sub>](https://leeporte.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=LeePorte "Code") | [<img src="https://avatars.githubusercontent.com/u/23613427?v=4" width="110px;"/><br /><sub>BRYAN </sub>](https://github.com/bryanlopezinc)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Code") [⚠️](https://github.com/snipe/snipe-it/commits?author=bryanlopezinc "Tests") |
| [<img src="https://avatars.githubusercontent.com/u/64061710?v=4" width="110px;"/><br /><sub>U-H-T</sub>](https://github.com/U-H-T)<br />[💻](https://github.com/snipe/snipe-it/commits?author=U-H-T "Code") | [<img src="https://avatars.githubusercontent.com/u/5395363?v=4" width="110px;"/><br /><sub>Matt Tyree</sub>](https://github.com/Tyree)<br />[📖](https://github.com/snipe/snipe-it/commits?author=Tyree "Documentation") | [<img src="https://avatars.githubusercontent.com/u/292081?v=4" width="110px;"/><br /><sub>Florent Bervas</sub>](http://spoontux.net)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FlorentDotMe "Code") | [<img src="https://avatars.githubusercontent.com/u/4498077?v=4" width="110px;"/><br /><sub>Daniel Albertsen</sub>](https://ditscheri.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=dbakan "Code") | [<img src="https://avatars.githubusercontent.com/u/100710244?v=4" width="110px;"/><br /><sub>r-xyz</sub>](https://github.com/r-xyz)<br />[💻](https://github.com/snipe/snipe-it/commits?author=r-xyz "Code") | [<img src="https://avatars.githubusercontent.com/u/47491036?v=4" width="110px;"/><br /><sub>Steven Mainor</sub>](https://github.com/DrekiDegga)<br />[💻](https://github.com/snipe/snipe-it/commits?author=DrekiDegga "Code") | [<img src="https://avatars.githubusercontent.com/u/65785975?v=4" width="110px;"/><br /><sub>arne-kroeger</sub>](https://github.com/arne-kroeger)<br />[💻](https://github.com/snipe/snipe-it/commits?author=arne-kroeger "Code") |
| [<img src="https://avatars.githubusercontent.com/u/167117705?v=4" width="110px;"/><br /><sub>Glukose1</sub>](https://github.com/Glukose1)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Glukose1 "Code") | [<img src="https://avatars.githubusercontent.com/u/1197791?v=4" width="110px;"/><br /><sub>Scarzy</sub>](https://github.com/Scarzy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Scarzy "Code") | [<img src="https://avatars.githubusercontent.com/u/37372069?v=4" width="110px;"/><br /><sub>setpill</sub>](https://github.com/setpill)<br />[💻](https://github.com/snipe/snipe-it/commits?author=setpill "Code") | [<img src="https://avatars.githubusercontent.com/u/3755203?v=4" width="110px;"/><br /><sub>swift2512</sub>](https://github.com/swift2512)<br />[🐛](https://github.com/snipe/snipe-it/issues?q=author%3Aswift2512 "Bug reports") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View file

@ -79,12 +79,12 @@ USER root
VOLUME ["/var/lib/snipeit"]
# Entrypoints
COPY docker/entrypoint_alpine.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Startup script
COPY docker/startup_alpine.sh /startup.sh
RUN chmod +x /startup.sh
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/entrypoint.sh"]
CMD ["/startup.sh"]
EXPOSE 80

View file

@ -97,7 +97,7 @@ RUN set -eux; \
VOLUME [ "/var/lib/snipeit" ]
COPY --chown=www-data:www-data docker/docker-secrets.env /var/www/html/.env
COPY --chmod=655 docker/docker-entrypoint.sh /usr/local/bin/docker-snipeit-entrypoint
COPY --chmod=655 docker/startup_alpine_fpm.sh /startup.sh
COPY docker/column-statistics.cnf /etc/mysql/conf.d/column-statistics.cnf
ENTRYPOINT [ "/usr/local/bin/docker-snipeit-entrypoint" ]
CMD [ "/usr/local/bin/docker-php-entrypoint", "php-fpm" ]
ENTRYPOINT [ "/startup.sh" ]
CMD [ "/startup.sh", "php-fpm" ]

View file

@ -72,12 +72,13 @@ Since the release of the JSON REST API, several third-party developers have been
- [Snipe-IT plugin for Jira Service Desk](https://marketplace.atlassian.com/apps/1220964/snipe-it-for-jira)
- [Python 3 CSV importer](https://github.com/gastamper/snipeit-csvimporter) - allows importing assets into Snipe-IT based on Item Name rather than Asset Tag.
- [Snipe-IT Kubernetes Helm Chart](https://github.com/t3n/helm-charts/tree/master/snipeit) - For more information, [click here](https://hub.helm.sh/charts/t3n/snipeit).
- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-it.
- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT
- [Snipe-IT Bulk Edit](https://github.com/bricelabelle/snipe-it-bulkedit) - Google Script files to use Google Sheets as a bulk checkout/checkin/edit tool for Snipe-IT.
- [MosyleSnipeSync](https://github.com/RodneyLeeBrands/MosyleSnipeSync) by [@Karpadiem](https://github.com/Karpadiem) - Python script to synchronize information between Mosyle and Snipe-IT.
- [WWW::SnipeIT](https://github.com/SEDC/perl-www-snipeit) by [@SEDC](https://github.com/SEDC) - perl module for accessing the API
- [UniFi to Snipe-IT](https://github.com/RodneyLeeBrands/UnifiSnipeSync) by [@karpadiem](https://github.com/karpadiem) - Python script that synchronizes UniFi devices with Snipe-IT.
- [Kandji2Snipe](https://github.com/grokability/kandji2snipe) by [@briangoldstein](https://github.com/briangoldstein) - Python script that synchronizes Kandji with Snipe-IT.
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by @ReticentRobot - Windows agent for Snipe-IT
- [SnipeAgent](https://github.com/ReticentRobot/SnipeAgent) by [@ReticentRobot](https://github.com/ReticentRobot) - Windows agent for Snipe-IT.
- [Gate Pass Generator](https://github.com/cha7uraAE/snipe-it-gate-pass-system) by [@cha7uraAE](https://github.com/cha7uraAE) - A Streamlit application for generating gate passes based on hardware data from a Snipe-IT API.
-----

View file

@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands;
use App\Models\Asset;
use App\Models\AssetModel;
use Illuminate\Console\Command;
class RemoveExplicitEols extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:remove-explicit-eols {--model_name= : The name of the asset model to update (use "all" to update all models)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Removes explicit EOLs on assets with selected model so they may inherit the asset model EOL';
/**
* Execute the console command.
*/
public function handle()
{
$startTime = microtime(true);
if ($this->option('model_name') == 'all') {
$assets = Asset::all();
$this->updateAssets($assets);
} else {
$assetModel = AssetModel::where('name', '=', $this->option('model_name'))->first();
if ($assetModel) {
$assets = Asset::where('model_id', '=', $assetModel->id)->get();
$this->updateAssets($assets);
} else {
$this->error('Asset model not found');
}
}
$endTime = microtime(true);
$executionTime = ($endTime - $startTime);
$this->info('Command executed in ' . round($executionTime, 2) . ' seconds.');
}
private function updateAssets($assets)
{
foreach ($assets as $asset) {
$asset->eol_explicit = 0;
$asset->asset_eol_date = null;
$asset->save();
}
$this->info($assets->count() . ' Assets updated successfully');
}
}

View file

@ -47,9 +47,10 @@ class SendAcceptanceReminder extends Command
{
$pending = CheckoutAcceptance::pending()->where('checkoutable_type', 'App\Models\Asset')
->whereHas('checkoutable', function($query) {
$query->where('archived', 0);
$query->where('accepted_at', null)
->where('declined_at', null);
})
->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model', 'checkoutable.adminuser'])
->with(['assignedTo', 'checkoutable.assignedTo', 'checkoutable.model', 'checkoutable.admin'])
->get();
$count = 0;

190
app/Helpers/IconHelper.php Normal file
View file

@ -0,0 +1,190 @@
<?php
namespace App\Helpers;
class IconHelper
{
public static function icon($type) {
switch ($type) {
case 'checkout':
return 'fa-solid fa-rotate-left';
case 'checkin':
return 'fa-solid fa-rotate-right';
case 'edit':
return 'fas fa-pencil-alt';
case 'clone':
return 'far fa-clone';
case 'delete':
return 'fas fa-trash';
case 'create':
return 'fa-solid fa-plus';
case 'audit':
return 'fa-solid fa-clipboard-check';
case '2fa reset':
return 'fa-solid fa-mobile-screen';
case 'new-user':
return 'fa-solid fa-user-plus';
case 'merged-user':
return 'fa-solid fa-people-arrows';
case 'delete-user':
return 'fa-solid fa-user-minus';
case 'update-user':
return 'fa-solid fa-user-pen';
case 'user':
return 'fa-solid fa-user';
case 'users':
return 'fas fa-users';
case 'restore':
return 'fa-solid fa-trash-arrow-up';
case 'external-link':
return 'fa fa-external-link';
case 'email':
return 'fa-regular fa-envelope';
case 'phone':
return 'fa-solid fa-phone';
case 'long-arrow-right':
return 'fas fa-long-arrow-alt-right';
case 'download':
return 'fas fa-download';
case 'checkmark':
return 'fas fa-check icon-white';
case 'x':
return 'fas fa-times';
case 'logout':
return 'fa fa-sign-out';
case 'admin-settings':
return 'fas fa-cogs';
case 'settings':
return 'fas fa-cog';
case 'angle-left':
return 'fas fa-angle-left';
case 'warning':
return 'fas fa-exclamation-triangle';
case 'kits':
return 'fas fa-object-group';
case 'assets':
case 'asset':
return 'fas fa-barcode';
case 'accessories':
case 'accessory':
return 'far fa-keyboard';
case 'components':
case 'component':
return 'far fa-hdd';
case 'consumables':
case 'consumable':
return 'fas fa-tint';
case 'licenses':
case 'license':
return 'far fa-save';
case 'requestable':
return 'fas fa-laptop';
case 'reports':
return 'fas fa-chart-bar';
case 'heart':
return 'fas fa-heart';
case 'circle':
return 'fa-regular fa-circle';
case 'circle-solid':
return 'fa-solid fa-circle';
case 'due':
return 'fas fa-history';
case 'import':
return 'fas fa-cloud-upload-alt';
case 'search':
return 'fas fa-search';
case 'alerts':
return 'far fa-flag';
case 'password':
return 'fa-solid fa-key';
case 'api-key':
return 'fa-solid fa-user-secret';
case 'nav-toggle':
return 'fas fa-bars';
case 'dashboard':
return 'fas fa-tachometer-alt';
case 'info-circle':
return 'fas fa-info-circle';
case 'caret-right':
return 'fa fa-caret-right';
case 'caret-up':
return 'fa fa-caret-up';
case 'caret-down':
return 'fa fa-caret-down';
case 'arrow-circle-right':
return 'fa fa-arrow-circle-right';
case 'minus':
return 'fas fa-minus';
case 'spinner':
return 'fas fa-spinner fa-spin';
case 'copy-clipboard':
return 'fa-regular fa-clipboard';
case 'paperclip':
return 'fas fa-paperclip';
case 'files':
return 'fa-regular fa-file';
case 'more-info':
return 'far fa-life-ring';
case 'calendar':
return 'fas fa-calendar';
case 'plus':
return 'fas fa-plus';
case 'history':
return 'fas fa-history';
case 'more-files':
return 'fa-solid fa-laptop-file';
case 'maintenances':
return 'fas fa-wrench';
case 'seats':
return 'far fa-list-alt';
case 'globe-us':
return 'fas fa-globe-americas';
case 'locked':
return 'fas fa-lock';
case 'unlocked':
return 'fas fa-lock';
case 'locations':
return 'fas fa-map-marker-alt';
case 'location':
return 'fas fa-map-marker-alt';
case 'superadmin':
return 'fas fa-crown';
case 'print':
return 'fa-solid fa-print';
case 'checkin-and-delete':
return 'fa-solid fa-user-xmark';
case 'branding':
return 'fas fa-copyright';
case 'general-settings':
return 'fa-solid fa-list-check';
case 'groups':
return 'fa-solid fa-user-group';
case 'bell':
return 'fa-solid fa-bell';
case 'hashtag':
return 'fa-solid fa-hashtag';
case 'asset-tags':
return 'fas fa-list-ol';
case 'labels':
return 'fas fa-tags';
case 'ldap':
return 'fas fa-sitemap';
case 'google':
return 'fa-brands fa-google';
case 'saml':
return 'fas fa-sign-in-alt';
case 'backups':
return 'fas fa-file-archive';
case 'logins':
return 'fas fa-crosshairs';
case 'oauth':
return 'fas fa-user-secret';
case 'employee_num' :
return 'fa-regular fa-id-card';
case 'department' :
return 'fa-solid fa-building-user';
}
}
}

View file

@ -0,0 +1,200 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\AssetModel;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* 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 AssetModelFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetModelId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function store(UploadFileRequest $request, $assetModelId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// Make sure we are allowed to update this asset
$this->authorize('update', $assetModel);
if ($request->hasFile('file')) {
// If the file storage directory doesn't exist; create it
if (! Storage::exists('private_uploads/assetmodels')) {
Storage::makeDirectory('private_uploads/assetmodels', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$assetModel->id, $file);
$assetModel->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $assetModel, trans('admin/models/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/models/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetModelId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function list($assetModelId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('view', $assetModel);
// Check that there are some uploads on this asset that can be listed
if ($assetModel->uploads->count() > 0) {
$files = array();
foreach ($assetModel->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/models/message.upload.success')));
}
// There are no files.
return response()->json(Helper::formatStandardApiResponse('success', array(), trans('admin/models/message.upload.success')));
}
// Send back an error message
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error')), 500);
}
/**
* Check for permissions and display the file.
*
* @param int $assetModelId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v7.0.12]
* @author [r-xyz]
*/
public function show($assetModelId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('view', $assetModel);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $assetModel->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assetmodels/'.$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/models/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/models/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetModelId
* @param int $fileId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function destroy($assetModelId = null, $fileId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetModelId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
$rel_path = 'private_uploads/assetmodels';
// the asset is valid
if (isset($assetModel->id)) {
$this->authorize('update', $assetModel);
// 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/models/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/models/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500);
}
}

View file

@ -78,6 +78,10 @@ class AssetModelsController extends Controller
$assetmodels = $assetmodels->where('models.category_id', '=', $request->input('category_id'));
}
if ($request->filled('depreciation_id')) {
$assetmodels = $assetmodels->where('models.depreciation_id', '=', $request->input('depreciation_id'));
}
if ($request->filled('search')) {
$assetmodels->TextSearch($request->input('search'));
}

View file

@ -602,7 +602,7 @@ class AssetsController extends Controller
if ($field->field_encrypted == '1') {
Log::debug('This model field is encrypted in this fieldset.');
if (Gate::allows('admin')) {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
// If input value is null, use custom field's default value
if (($field_val == null) && ($request->has('model_id') != '')) {
@ -695,7 +695,7 @@ class AssetsController extends Controller
}
}
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
$field_val = Crypt::encrypt($field_val);
} else {
$problems_updating_encrypted_custom_fields = true;
@ -928,7 +928,7 @@ class AssetsController extends Controller
}
}
if ($request->has('status_id')) {
if ($request->filled('status_id')) {
$asset->status_id = $request->input('status_id');
}
@ -978,7 +978,7 @@ class AssetsController extends Controller
public function checkinByTag(Request $request, $tag = null) : JsonResponse
{
$this->authorize('checkin', Asset::class);
if(null == $tag && null !== ($request->input('asset_tag'))) {
if (null == $tag && null !== ($request->input('asset_tag'))) {
$tag = $request->input('asset_tag');
}
$asset = Asset::where('asset_tag', $tag)->first();

View file

@ -86,6 +86,9 @@ class ConsumablesController extends Controller
case 'company':
$consumables = $consumables->OrderCompany($order);
break;
case 'remaining':
$consumables = $consumables->OrderRemaining($order);
break;
case 'supplier':
$consumables = $consumables->OrderSupplier($order);
break;

View file

@ -20,9 +20,22 @@ class DepreciationsController extends Controller
public function index(Request $request) : JsonResponse | array
{
$this->authorize('view', Depreciation::class);
$allowed_columns = ['id','name','months','depreciation_min', 'depreciation_type','created_at'];
$allowed_columns = [
'id',
'name',
'months',
'depreciation_min',
'depreciation_type',
'created_at',
'assets_count',
'models_count',
'licenses_count',
];
$depreciations = Depreciation::select('id','name','months','depreciation_min','depreciation_type','user_id','created_at','updated_at');
$depreciations = Depreciation::select('id','name','months','depreciation_min','depreciation_type','user_id','created_at','updated_at')
->withCount('assets as assets_count')
->withCount('models as models_count')
->withCount('licenses as licenses_count');
if ($request->filled('search')) {
$depreciations = $depreciations->TextSearch($request->input('search'));

View file

@ -27,7 +27,7 @@ class LicensesController extends Controller
$licenses = License::with('company', 'manufacturer', 'supplier','category', 'adminuser')->withCount('freeSeats as free_seats_count');
if ($request->filled('company_id')) {
$licenses->where('company_id', '=', $request->input('company_id'));
$licenses->where('licenses.company_id', '=', $request->input('company_id'));
}
if ($request->filled('name')) {

View file

@ -246,7 +246,7 @@ class PredefinedKitsController extends Controller
$relation = $kit->models();
if ($relation->find($model_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, ['model' => 'Model already attached to kit']));
return response()->json(Helper::formatStandardApiResponse('error', null, ['model' => trans('admin/kits/general.model_already_attached')]));
}
$relation->attach($model_id, ['quantity' => $quantity]);

View file

@ -427,13 +427,10 @@ class UsersController extends Controller
* @param \Illuminate\Http\Request $request
* @param int $id
*/
public function update(SaveUserRequest $request, $id) : JsonResponse
public function update(SaveUserRequest $request, User $user): JsonResponse
{
$this->authorize('update', User::class);
if ($user = User::find($id)) {
$this->authorize('update', $user);
/**
@ -443,12 +440,10 @@ class UsersController extends Controller
*
*/
if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) {
if ((($user->id == 1) || ($user->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')) {
@ -473,16 +468,13 @@ class UsersController extends Controller
$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())) {
@ -496,20 +488,12 @@ class UsersController extends Controller
// 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()));
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id'))));
}
/**
* Remove the specified resource from storage.
*

View file

@ -202,6 +202,7 @@ class AssetModelsController extends Controller
if ($model->image) {
try {
Storage::disk('public')->delete('models/'.$model->image);
$model->update(['image' => null]);
} catch (\Exception $e) {
Log::info($e);
}
@ -233,7 +234,7 @@ class AssetModelsController extends Controller
if ($model->restore()) {
$logaction = new Actionlog();
$logaction->item_type = User::class;
$logaction->item_type = AssetModel::class;
$logaction->item_id = $model->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = auth()->id();

View file

@ -165,7 +165,7 @@ class AssetsController extends Controller
if (($model) && ($model->fieldset)) {
foreach ($model->fieldset->fields as $field) {
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
@ -388,7 +388,7 @@ class AssetsController extends Controller
foreach ($model->fieldset->fields as $field) {
if ($field->field_encrypted == '1') {
if (Gate::allows('admin')) {
if (Gate::allows('assets.view.encrypted_custom_fields')) {
if (is_array($request->input($field->db_column))) {
$asset->{$field->db_column} = Crypt::encrypt(implode(', ', $request->input($field->db_column)));
} else {
@ -844,7 +844,7 @@ class AssetsController extends Controller
{
$this->authorize('checkin', Asset::class);
return view('hardware/quickscan-checkin');
return view('hardware/quickscan-checkin')->with('statusLabel_list', Helper::statusLabelList());
}
public function audit($id)

View file

@ -227,7 +227,8 @@ class BulkAssetsController extends Controller
* its checkout status.
*/
if (($request->filled('purchase_date'))
if (($request->filled('name'))
|| ($request->filled('purchase_date'))
|| ($request->filled('expected_checkin'))
|| ($request->filled('purchase_cost'))
|| ($request->filled('supplier_id'))
@ -239,6 +240,7 @@ class BulkAssetsController extends Controller
|| ($request->filled('status_id'))
|| ($request->filled('model_id'))
|| ($request->filled('next_audit_date'))
|| ($request->filled('null_name'))
|| ($request->filled('null_purchase_date'))
|| ($request->filled('null_expected_checkin_date'))
|| ($request->filled('null_next_audit_date'))
@ -251,13 +253,14 @@ class BulkAssetsController extends Controller
$this->update_array = [];
/**
* Leave out model_id and status here because we do math on that later. We have to do some extra
* validation and checks on those two.
* Leave out model_id and status here because we do math on that later. We have to do some
* extra validation and checks on those two.
*
* It's tempting to make these match the request check above, but some of these values require
* extra work to make sure the data makes sense.
*/
$this->conditionallyAddItem('purchase_date')
$this->conditionallyAddItem('name')
->conditionallyAddItem('purchase_date')
->conditionallyAddItem('expected_checkin')
->conditionallyAddItem('order_number')
->conditionallyAddItem('requestable')
@ -271,6 +274,11 @@ class BulkAssetsController extends Controller
/**
* Blank out fields that were requested to be blanked out via checkbox
*/
if ($request->input('null_name')=='1') {
$this->update_array['name'] = null;
}
if ($request->input('null_purchase_date')=='1') {
$this->update_array['purchase_date'] = null;
}

View file

@ -508,8 +508,8 @@ class LoginController extends Controller
protected function validator(array $data)
{
return Validator::make($data, [
'username' => 'required',
'password' => 'required',
'username' => 'required|not_array',
'password' => 'required|not_array',
]);
}

View file

@ -193,13 +193,20 @@ class DepreciationsController extends Controller
*/
public function show($id) : View | RedirectResponse
{
if (is_null($depreciation = Depreciation::find($id))) {
// Redirect to the blogs management page
return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist'));
}
$depreciation = Depreciation::withCount('assets as assets_count')
->withCount('models as models_count')
->withCount('licenses as licenses_count')
->find($id);
$this->authorize('view', $depreciation);
if ($depreciation) {
return view('depreciations/view', compact('depreciation'));
}
return redirect()->route('depreciations.index')->with('error', trans('admin/depreciations/message.does_not_exist'));
}
}

View file

@ -62,10 +62,10 @@ class CheckoutKitController extends Controller
$checkout_result = $this->kitService->checkout($request, $kit, $user);
if (Arr::has($checkout_result, 'errors') && count($checkout_result['errors']) > 0) {
return redirect()->back()->with('error', trans('general.checkout_error'))->with('error_messages', $checkout_result['errors']);
return redirect()->back()->with('error', trans('admin/kits/general.checkout_error'))->with('error_messages', $checkout_result['errors']);
}
return redirect()->back()->with('success', trans('general.checkout_success'))
return redirect()->back()->with('success', trans('admin/kits/general.checkout_success'))
->with('assets', Arr::get($checkout_result, 'assets', null))
->with('accessories', Arr::get($checkout_result, 'accessories', null))
->with('consumables', Arr::get($checkout_result, 'consumables', null));

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Http\Requests\ImageUploadRequest;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\Location;
use App\Models\User;
@ -193,7 +194,13 @@ class LocationsController extends Controller
*/
public function show($id = null) : View | RedirectResponse
{
$location = Location::find($id);
$location = Location::withCount('assignedAssets as assigned_assets_count')
->withCount('assets as assets_count')
->withCount('rtd_assets as rtd_assets_count')
->withCount('children as children_count')
->withCount('users as users_count')
->withTrashed()
->find($id);
if (isset($location->id)) {
return view('locations/view', compact('location'));
@ -249,6 +256,41 @@ class LocationsController extends Controller
}
/**
* Restore a given Asset Model (mark as un-deleted)
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $id
*/
public function postRestore($id) : RedirectResponse
{
$this->authorize('create', Location::class);
if ($location = Location::withTrashed()->find($id)) {
if ($location->deleted_at == '') {
return redirect()->back()->with('error', trans('general.not_deleted', ['item_type' => trans('general.location')]));
}
if ($location->restore()) {
$logaction = new Actionlog();
$logaction->item_type = Location::class;
$logaction->item_id = $location->id;
$logaction->created_at = date('Y-m-d H:i:s');
$logaction->user_id = auth()->id();
$logaction->logaction('restore');
return redirect()->route('locations.index')->with('success', trans('admin/locations/message.restore.success'));
}
// Check validation
return redirect()->back()->with('error', trans('general.could_not_restore', ['item_type' => trans('general.location'), 'error' => $location->getErrors()->first()]));
}
return redirect()->back()->with('error', trans('admin/models/message.does_not_exist'));
}
public function print_all_assigned($id) : View | RedirectResponse
{
if ($location = Location::where('id', $id)->first()) {

View file

@ -50,6 +50,7 @@ class ProfileController extends Controller
$user->skin = $request->input('skin');
$user->phone = $request->input('phone');
$user->enable_sounds = $request->input('enable_sounds', false);
$user->enable_confetti = $request->input('enable_confetti', false);
if (! config('app.lock_passwords')) {
$user->locale = $request->input('locale', 'en-US');

View file

@ -637,6 +637,7 @@ class SettingsController extends Controller
$setting->alert_threshold = $request->input('alert_threshold');
$setting->audit_interval = $request->input('audit_interval');
$setting->audit_warning_days = $request->input('audit_warning_days');
$setting->due_checkin_days = $request->input('due_checkin_days');
$setting->show_alerts_in_menu = $request->input('show_alerts_in_menu', '0');
if ($setting->save()) {
@ -1203,7 +1204,7 @@ class SettingsController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v6.0]
*/
public function postRestore($filename = null) : RedirectResponse
public function postRestore(Request $request, $filename = null): RedirectResponse
{
if (! config('app.lock_passwords')) {
@ -1223,13 +1224,29 @@ class SettingsController extends Controller
Log::debug('Attempting to restore from: '. storage_path($path).'/'.$filename);
// run the restore command
Artisan::call('snipeit:restore',
[
$restore_params = [
'--force' => true,
'--no-progress' => true,
'filename' => storage_path($path).'/'.$filename
'filename' => storage_path($path) . '/' . $filename
];
if ($request->input('clean')) {
Log::debug("Attempting 'clean' - first, guessing prefix...");
Artisan::call('snipeit:restore', [
'--sanitize-guess-prefix' => true,
'filename' => storage_path($path) . '/' . $filename
]);
$guess_prefix_output = Artisan::output();
Log::debug("Sanitize output is: $guess_prefix_output");
list($prefix, $_output) = explode("\n", $guess_prefix_output);
Log::debug("prefix is: '$prefix'");
$restore_params['--sanitize-with-prefix'] = $prefix;
}
// run the restore command
Artisan::call('snipeit:restore',
$restore_params
);
// If it's greater than 300, it probably worked
$output = Artisan::output();

View file

@ -30,7 +30,7 @@ class BulkUsersController extends Controller
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.7]
* @param Request $request
* @return \Illuminate\Contracts\View\View
* @return \Illuminate\Contracts\View\View | \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function edit(Request $request)
@ -116,6 +116,9 @@ class BulkUsersController extends Controller
->conditionallyAddItem('remote')
->conditionallyAddItem('ldap_import')
->conditionallyAddItem('activated')
->conditionallyAddItem('start_date')
->conditionallyAddItem('end_date')
->conditionallyAddItem('city')
->conditionallyAddItem('autoassign_licenses');
@ -146,6 +149,13 @@ class BulkUsersController extends Controller
$this->update_array['company_id'] = null;
}
if ($request->input('null_start_date')=='1') {
$this->update_array['start_date'] = null;
}
if ($request->input('null_end_date')=='1') {
$this->update_array['end_date'] = null;
}
if (! $manager_conflict) {
$this->conditionallyAddItem('manager_id');

View file

@ -186,7 +186,7 @@ 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()->find($id);
if ($user) {
@ -214,28 +214,24 @@ class UsersController extends Controller
* @return \Illuminate\Http\RedirectResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(SaveUserRequest $request, $id = null)
public function update(SaveUserRequest $request, User $user)
{
$this->authorize('update', User::class);
// This is a janky hack to prevent people from changing admin demo user data on the public demo.
// The $ids 1 and 2 are special since they are seeded as superadmins in the demo seeder.
// Thanks, jerks. You are why we can't have nice things. - snipe
if ((($id == 1) || ($id == 2)) && (config('app.lock_passwords'))) {
if ((($user->id == 1) || ($user->id == 2)) && (config('app.lock_passwords'))) {
return redirect()->route('users.index')->with('error', trans('general.permission_denied_superuser_demo'));
}
// We need to reverse the UI specific logic for our
// permissions here before we update the user.
$permissions = $request->input('permissions', []);
app('request')->request->set('permissions', $permissions);
$user = User::with('assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc')->withTrashed()->find($id);
$user->load(['assets', 'assets.model', 'consumables', 'accessories', 'licenses', 'userloc'])->withTrashed();
// User is valid - continue...
if ($user) {
$this->authorize('update', $user);
// Figure out of this user was an admin before this edit
@ -318,13 +314,7 @@ class UsersController extends Controller
return redirect()->to(Helper::getRedirectOption($request, $user->id, 'Users'))
->with('success', trans('admin/users/message.success.update'));
}
return redirect()->back()->withInput()->withErrors($user->getErrors());
}
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
/**
@ -601,19 +591,15 @@ class UsersController extends Controller
/**
* Print inventory
*
* @author Aladin Alaily
* @since [v1.8]
* @return \Illuminate\Http\RedirectResponse
* @author Aladin Alaily
*/
public function printInventory($id)
{
$this->authorize('view', User::class);
$user = User::where('id', $id)->withTrashed()->first();
if ($user = User::where('id', $id)->withTrashed()->first()) {
// Make sure they can view this particular user
$this->authorize('view', $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();
@ -626,6 +612,10 @@ class UsersController extends Controller
->with('settings', Setting::getSettings());
}
return redirect()->route('users.index')->with('error', trans('admin/users/message.user_not_found', compact('id')));
}
/**
* Emails user a list of assigned assets
*

View file

@ -14,6 +14,7 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\NoSessionStore::class,
\Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Session\Middleware\StartSession::class,
@ -21,6 +22,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\CheckForSetup::class,
\App\Http\Middleware\CheckForDebug::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrimStrings::class,
\App\Http\Middleware\SecurityHeaders::class,
\App\Http\Middleware\PreventBackHistory::class,
\Illuminate\Http\Middleware\HandleCors::class,
@ -48,6 +50,7 @@ class Kernel extends HttpKernel
'api' => [
'auth:api',
\App\Http\Middleware\CheckLocale::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];

View file

@ -6,6 +6,7 @@ use App\Models\Setting;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use App\Rules\UserCannotSwitchCompaniesIfItemsAssigned;
class SaveUserRequest extends FormRequest
{
@ -34,6 +35,7 @@ class SaveUserRequest extends FormRequest
$rules = [
'department_id' => 'nullable|exists:departments,id',
'manager_id' => 'nullable|exists:users,id',
'company_id' => ['nullable','exists:companies,id']
];
switch ($this->method()) {
@ -52,11 +54,13 @@ class SaveUserRequest extends FormRequest
$rules['first_name'] = 'required|string|min:1';
$rules['username'] = 'required_unless:ldap_import,1|string|min:1';
$rules['password'] = Setting::passwordComplexityRulesSaving('update').'|confirmed';
$rules['company_id'] = [new UserCannotSwitchCompaniesIfItemsAssigned()];
break;
// Save only what's passed
case 'PATCH':
$rules['password'] = Setting::passwordComplexityRulesSaving('update');
$rules['company_id'] = [new UserCannotSwitchCompaniesIfItemsAssigned()];
break;
default:

View file

@ -4,6 +4,7 @@ namespace App\Http\Requests;
use App\Http\Requests\Traits\MayContainCustomFields;
use App\Models\Asset;
use App\Models\Setting;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
@ -41,6 +42,12 @@ class UpdateAssetRequest extends ImageUploadRequest
],
);
// if the purchase cost is passed in as a string **and** the digit_separator is ',' (as is common in the EU)
// then we tweak the purchase_cost rule to make it a string
if (Setting::getSettings()->digit_separator === '1.234,56' && is_string($this->input('purchase_cost'))) {
$rules['purchase_cost'] = ['nullable', 'string'];
}
return $rules;
}
}

View file

@ -11,15 +11,17 @@ trait TwoColumnUniqueUndeletedTrait
* @param string $field
* @return string
*/
protected function prepareTwoColumnUniqueUndeletedRule($parameters, $field)
protected function prepareTwoColumnUniqueUndeletedRule($parameters)
{
$column = $parameters[0];
$value = $this->{$parameters[0]};
// This is an existing model we're updating so ignore the current ID ($this->getKey())
if ($this->exists) {
return 'two_column_unique_undeleted:'.$this->table.','.$this->getKey().','.$column.','.$value;
}
// This is a new record, so we can ignore the current ID
return 'two_column_unique_undeleted:'.$this->table.',0,'.$column.','.$value;
}
}

View file

@ -86,7 +86,7 @@ class AssetsTransformer
'next_audit_date' => Helper::getFormattedDateObject($asset->next_audit_date, 'date'),
'deleted_at' => Helper::getFormattedDateObject($asset->deleted_at, 'datetime'),
'purchase_date' => Helper::getFormattedDateObject($asset->purchase_date, 'date'),
'age' => $asset->purchase_date ? $asset->purchase_date->diffForHumans() : '',
'age' => $asset->purchase_date ? $asset->purchase_date->locale(app()->getLocale())->diffForHumans() : '',
'last_checkout' => Helper::getFormattedDateObject($asset->last_checkout, 'datetime'),
'last_checkin' => Helper::getFormattedDateObject($asset->last_checkin, 'datetime'),
'expected_checkin' => Helper::getFormattedDateObject($asset->expected_checkin, 'date'),

View file

@ -28,6 +28,9 @@ class DepreciationsTransformer
'name' => e($depreciation->name),
'months' => $depreciation->months.' '.trans('general.months'),
'depreciation_min' => $depreciation->depreciation_type === 'percent' ? $depreciation->depreciation_min.'%' : $depreciation->depreciation_min,
'assets_count' => $depreciation->assets_count,
'models_count' => $depreciation->models_count,
'licenses_count' => $depreciation->licenses_count,
'created_at' => Helper::getFormattedDateObject($depreciation->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($depreciation->updated_at, 'datetime')
];

View file

@ -71,8 +71,10 @@ class AssetImporter extends ItemImporter
$asset = Asset::where(['asset_tag'=> (string) $asset_tag])->first();
if ($asset) {
if (! $this->updating) {
$this->log('A matching Asset '.$asset_tag.' already exists');
return;
$exists_error = trans('general.import_asset_tag_exists', ['asset_tag' => $asset_tag]);
$this->log($exists_error);
$this->addErrorToBag($asset, 'asset_tag', $exists_error);
return $exists_error;
}
$this->log('Updating Asset');

View file

@ -281,6 +281,13 @@ abstract class Importer
}
}
protected function addErrorToBag($item, $field, $error_message)
{
if ($this->errorCallback) {
call_user_func($this->errorCallback, $item, $field, [$field => [$error_message]]);
}
}
/**
* Finds the user matching given data, or creates a new one if there is no match.
* This is NOT used by the User Import, only for Asset/Accessory/etc where

View file

@ -196,39 +196,46 @@ class ItemImporter extends Importer
{
$condition = array();
$asset_model_name = $this->findCsvMatch($row, 'asset_model');
$asset_model_category = $this->findCsvMatch($row, 'category');
$asset_modelNumber = $this->findCsvMatch($row, 'model_number');
// TODO: At the moment, this means we can't update the model number if the model name stays the same.
if (! $this->shouldUpdateField($asset_model_name)) {
return;
}
if ((empty($asset_model_name)) && (! empty($asset_modelNumber))) {
$asset_model_name = $asset_modelNumber;
} elseif ((empty($asset_model_name)) && (empty($asset_modelNumber))) {
$asset_model_name = 'Unknown';
}
if ((!empty($asset_model_name)) && (empty($asset_modelNumber))) {
$condition[] = ['name', '=', $asset_model_name];
} elseif ((!empty($asset_model_name)) && (!empty($asset_modelNumber))) {
$condition[] = ['name', '=', $asset_model_name];
$condition[] = ['model_number', '=', $asset_modelNumber];
$asset_model = AssetModel::select('id');
if (!empty($asset_model_name)) {
$asset_model = $asset_model->where('name', '=', $asset_model_name);
if (!empty($asset_modelNumber)) {
$asset_model = $asset_model->where('model_number', '=', $asset_modelNumber);
}
}
$editingModel = $this->updating;
$asset_model = AssetModel::where($condition)->first();
$asset_model = $asset_model->first();
if ($asset_model) {
if (! $this->updating) {
$this->log('A matching model already exists, returning it.');
return $asset_model->id;
}
$this->log('Matching Model found, updating it.');
$item = $this->sanitizeItemForStoring($asset_model, $editingModel);
$item['name'] = $asset_model_name;
$item['notes'] = $this->findCsvMatch($row, 'model_notes');
if(!empty($asset_modelNumber)){
if (!empty($asset_modelNumber)){
$item['model_number'] = $asset_modelNumber;
}
@ -237,23 +244,29 @@ class ItemImporter extends Importer
$this->log('Asset Model Updated');
return $asset_model->id;
}
$this->log('No Matching Model, Creating a new one');
}
$this->log('No Matching Model, Creating a new one');
$asset_model = new AssetModel();
$item = $this->sanitizeItemForStoring($asset_model, $editingModel);
$item['name'] = $asset_model_name;
$item['model_number'] = $asset_modelNumber;
$item['notes'] = $this->findCsvMatch($row, 'model_notes');
$item['category_id'] = $this->createOrFetchCategory($asset_model_category);
$asset_model->fill($item);
//$asset_model = AssetModel::firstOrNew($item);
$item = null;
if ($asset_model->save()) {
$this->log('Asset Model '.$asset_model_name.' with model number '.$asset_modelNumber.' was created');
return $asset_model->id;
}
$this->log('Asset Model Errors: '.$asset_model->getErrors());
$this->logError($asset_model, 'Asset Model "'.$asset_model_name.'"');
return null;

View file

@ -3,30 +3,25 @@
namespace App\Livewire;
use App\Models\CustomField;
use Livewire\Component;
use App\Models\Import;
use Illuminate\Support\Facades\Storage;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Computed;
use Livewire\Component;
class Importer extends Component
{
use AuthorizesRequests;
public $files;
public $progress; //upload progress - '-1' means don't show
public $progress = -1; //upload progress - '-1' means don't show
public $progress_message;
public $progress_bar_class;
public $progress_bar_class = 'progress-bar-warning';
public $message; //status/error message?
public $message_type; //success/error?
//originally from ImporterFile
public $import_errors; //
public ?Import $activeFile = null;
public $activeFileId;
public $headerRow = [];
public $typeOfImport;
public $importTypes;
public $columnOptions;
public $statusType;
@ -35,7 +30,6 @@ class Importer extends Component
public $send_welcome;
public $run_backup;
public $field_map; // we need a separate variable for the field-mapping, because the keys in the normal array are too complicated for Livewire to understand
public $file_id; // TODO: I can't figure out *why* we need this, but it really seems like we do. I can't seem to pull the id from the activeFile for some reason?
// Make these variables public - we set the properties in the constructor so we can localize them (versus the old static arrays)
public $accessories_fields;
@ -51,10 +45,8 @@ class Importer extends Component
'files.*.file_path' => 'required|string',
'files.*.created_at' => 'required|string',
'files.*.filesize' => 'required|integer',
'activeFile' => 'Import',
'activeFile.import_type' => 'string',
'activeFile.field_map' => 'array',
'activeFile.header_row' => 'array',
'headerRow' => 'array',
'typeOfImport' => 'string',
'field_map' => 'array'
];
@ -68,15 +60,13 @@ class Importer extends Component
{
$tmp = array();
if ($this->activeFile) {
$tmp = array_combine($this->activeFile->header_row, $this->field_map);
$tmp = array_combine($this->headerRow, $this->field_map);
$tmp = array_filter($tmp);
}
return json_encode($tmp);
}
private function getColumns($type)
{
switch ($type) {
@ -115,17 +105,15 @@ class Importer extends Component
return $results;
}
public function updating($name, $new_import_type)
public function updatingTypeOfImport($type)
{
if ($name == "activeFile.import_type") {
// go through each header, find a matching field to try and map it to.
foreach ($this->activeFile->header_row as $i => $header) {
foreach ($this->headerRow as $i => $header) {
// do we have something mapped already?
if (array_key_exists($i, $this->field_map)) {
// yes, we do. Is it valid for this type of import?
// (e.g. the import type might have been changed...?)
if (array_key_exists($this->field_map[$i], $this->columnOptions[$new_import_type])) {
if (array_key_exists($this->field_map[$i], $this->columnOptions[$type])) {
//yes, this key *is* valid. Continue on to the next field.
continue;
} else {
@ -135,9 +123,9 @@ class Importer extends Component
} // TODO - strictly speaking, this isn't necessary here I don't think.
}
// first, check for exact matches
foreach ($this->columnOptions[$new_import_type] as $value => $text) {
foreach ($this->columnOptions[$type] as $v => $text) {
if (strcasecmp($text, $header) === 0) { // case-INSENSITIVe on purpose!
$this->field_map[$i] = $value;
$this->field_map[$i] = $v;
continue 2; //don't bother with the alias check, go to the next header
}
}
@ -148,7 +136,7 @@ class Importer extends Component
// Make *absolutely* sure that this key actually _exists_ in this import type -
// you can trigger this by importing accessories with a 'Warranty' column (which don't exist
// in "Accessories"!)
if (array_key_exists($key, $this->columnOptions[$new_import_type])) {
if (array_key_exists($key, $this->columnOptions[$type])) {
$this->field_map[$i] = $key;
continue 3; // bust out of both of these loops; as well as the surrounding one - e.g. move on to the next header
}
@ -159,18 +147,10 @@ class Importer extends Component
$this->field_map[$i] = null; // Booooo :(
}
}
}
public function boot() { // FIXME - delete or undelete.
///////$this->activeFile = null; // I do *not* understand why I have to do this, but, well, whatever.
}
public function mount()
{
$this->authorize('import');
$this->progress = -1; // '-1' means 'don't show the progressbar'
$this->progress_bar_class = 'progress-bar-warning';
$this->importTypes = [
'asset' => trans('general.assets'),
'accessory' => trans('general.accessories'),
@ -374,6 +354,12 @@ class Importer extends Component
'model name',
'model',
],
'eol_date' =>
[
'eol',
'eol date',
'asset eol date',
],
'gravatar' =>
[
'gravatar',
@ -504,19 +490,16 @@ class Importer extends Component
];
$this->columnOptions[''] = $this->getColumns(''); //blank mode? I don't know what this is supposed to mean
foreach($this->importTypes AS $type => $name) {
foreach ($this->importTypes as $type => $name) {
$this->columnOptions[$type] = $this->getColumns($type);
}
if ($this->activeFile) {
$this->field_map = $this->activeFile->field_map ? array_values($this->activeFile->field_map) : [];
}
}
public function selectFile($id)
{
$this->clearMessage();
$this->activeFile = Import::find($id);
$this->activeFileId = $id;
if (!$this->activeFile) {
$this->message = trans('admin/hardware/message.import.file_missing');
@ -525,15 +508,17 @@ class Importer extends Component
return;
}
$this->headerRow = $this->activeFile->header_row;
$this->typeOfImport = $this->activeFile->import_type;
$this->field_map = null;
foreach($this->activeFile->header_row as $element) {
if(isset($this->activeFile->field_map[$element])) {
foreach ($this->headerRow as $element) {
if (isset($this->activeFile->field_map[$element])) {
$this->field_map[] = $this->activeFile->field_map[$element];
} else {
$this->field_map[] = null; // re-inject the 'nulls' if a file was imported with some 'Do Not Import' settings
}
}
$this->file_id = $id;
$this->import_errors = null;
$this->statusText = null;
@ -541,22 +526,34 @@ class Importer extends Component
public function destroy($id)
{
// 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)) {
$file->delete();
$this->authorize('import');
$import = Import::find($id);
// Check that the import wasn't deleted after while page was already loaded...
// @todo: next up...handle the file being missing for other interactions...
// for example having an import open in two tabs, deleting it, and then changing
// the import type in the other tab. The error message below wouldn't display in that case.
if (!$import) {
$this->message = trans('admin/hardware/message.import.file_already_deleted');
$this->message_type = 'danger';
return;
}
if (Storage::delete('private_uploads/imports/' . $import->file_path)) {
$import->delete();
$this->message = trans('admin/hardware/message.import.file_delete_success');
$this->message_type = 'success';
unset($this->files);
return;
} else {
}
$this->message = trans('admin/hardware/message.import.file_delete_error');
$this->message_type = 'danger';
}
}
}
}
public function clearMessage()
{
@ -564,9 +561,20 @@ class Importer extends Component
$this->message_type = null;
}
#[Computed]
public function files()
{
return Import::orderBy('id', 'desc')->get();
}
#[Computed]
public function activeFile()
{
return Import::find($this->activeFileId);
}
public function render()
{
$this->files = Import::orderBy('id','desc')->get(); //HACK - slows down renders.
return view('livewire.importer')
->extends('layouts.default')
->section('content');

View file

@ -1315,7 +1315,7 @@ class Asset extends Depreciable
public function scopeDueForCheckin($query, $settings)
{
$interval = $settings->audit_warning_days ?? 0;
$interval = $settings->due_checkin_days ?? 0;
$today = Carbon::now();
$interval_date = $today->copy()->addDays($interval)->format('Y-m-d');
@ -1561,7 +1561,7 @@ class Asset extends Depreciable
$leftJoin->on('assets_dept_users.id', '=', 'assets.assigned_to')
->where('assets.assigned_type', '=', User::class);
})->where(function ($query) use ($search) {
$query->where('assets_dept_users.department_id', '=', $search);
$query->whereIn('assets_dept_users.department_id', $search);
})->withTrashed()->whereNull('assets.deleted_at'); //workaround for laravel bug
}
@ -1811,7 +1811,9 @@ class Asset extends Depreciable
public function scopeInCategory($query, $category_id)
{
return $query->join('models as category_models', 'assets.model_id', '=', 'category_models.id')
->join('categories', 'category_models.category_id', '=', 'categories.id')->where('category_models.category_id', '=', $category_id);
->join('categories', 'category_models.category_id', '=', 'categories.id')
->whereIn('category_models.category_id', (!is_array($category_id) ? explode(',',$category_id): $category_id));
//->whereIn('category_models.category_id', $category_id);
}
/**
@ -1825,7 +1827,7 @@ class Asset extends Depreciable
public function scopeByManufacturer($query, $manufacturer_id)
{
return $query->join('models', 'assets.model_id', '=', 'models.id')
->join('manufacturers', 'models.manufacturer_id', '=', 'manufacturers.id')->where('models.manufacturer_id', '=', $manufacturer_id);
->join('manufacturers', 'models.manufacturer_id', '=', 'manufacturers.id')->whereIn('models.manufacturer_id', (!is_array($manufacturer_id) ? explode(',',$manufacturer_id): $manufacturer_id));
}

View file

@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Watson\Validating\ValidatingTrait;
use \App\Presenters\AssetModelPresenter;
use App\Http\Traits\TwoColumnUniqueUndeletedTrait;
/**
* Model for Asset Models. Asset Models contain higher level
@ -21,21 +22,8 @@ class AssetModel extends SnipeModel
{
use HasFactory;
use SoftDeletes;
protected $presenter = AssetModelPresenter::class;
use Loggable, Requestable, Presentable;
protected $table = 'models';
protected $hidden = ['user_id', 'deleted_at'];
// Declare the rules for the model validation
protected $rules = [
'name' => 'required|min:1|max:255',
'model_number' => 'max:255|nullable',
'min_amt' => 'integer|min:0|nullable',
'category_id' => 'required|integer|exists:categories,id',
'manufacturer_id' => 'integer|exists:manufacturers,id|nullable',
'eol' => 'integer:min:0|max:240|nullable',
];
use TwoColumnUniqueUndeletedTrait;
/**
* Whether the model should inject its identifier to the unique
@ -44,8 +32,26 @@ class AssetModel extends SnipeModel
*
* @var bool
*/
protected $injectUniqueIdentifier = true;
use ValidatingTrait;
protected $table = 'models';
protected $hidden = ['user_id', 'deleted_at'];
protected $presenter = AssetModelPresenter::class;
// Declare the rules for the model validation
protected $rules = [
'name' => 'string|required|min:1|max:255|two_column_unique_undeleted:model_number',
'model_number' => 'string|max:255|nullable|two_column_unique_undeleted:name',
'min_amt' => 'integer|min:0|nullable',
'category_id' => 'required|integer|exists:categories,id',
'manufacturer_id' => 'integer|exists:manufacturers,id|nullable',
'eol' => 'integer:min:0|max:240|nullable',
];
/**
* The attributes that are mass assignable.
@ -73,7 +79,12 @@ class AssetModel extends SnipeModel
*
* @var array
*/
protected $searchableAttributes = ['name', 'model_number', 'notes', 'eol'];
protected $searchableAttributes = [
'name',
'model_number',
'notes',
'eol'
];
/**
* The relations and their attributes that should be included when searching the model.
@ -86,6 +97,9 @@ class AssetModel extends SnipeModel
'manufacturer' => ['name'],
];
/**
* Establishes the model -> assets relationship
*

View file

@ -425,6 +425,20 @@ class Consumable extends SnipeModel
return $query->leftJoin('companies', 'consumables.company_id', '=', 'companies.id')->orderBy('companies.name', $order);
}
/**
* Query builder scope to order on remaining
*
* @param \Illuminate\Database\Query\Builder $query Query builder instance
* @param string $order Order
*
* @return \Illuminate\Database\Query\Builder Modified query builder
*/
public function scopeOrderRemaining($query, $order)
{
$order_by = 'consumables.qty - consumables_users_count ' . $order;
return $query->orderByRaw($order_by);
}
/**
* Query builder scope to order on supplier
*

View file

@ -75,4 +75,17 @@ class Depreciation extends SnipeModel
{
return $this->hasMany(\App\Models\License::class, 'depreciation_id');
}
/**
* Establishes the depreciation -> assets relationship
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v5.0]
* @return \Illuminate\Database\Eloquent\Relations\Relation
*/
public function assets()
{
return $this->hasManyThrough(\App\Models\Asset::class, \App\Models\AssetModel::class, 'depreciation_id', 'model_id');
}
}

View file

@ -33,7 +33,7 @@ class Location extends SnipeModel
'country' => 'min:2|max:191|nullable',
'zip' => 'max:10|nullable',
'manager_id' => 'exists:users,id|nullable',
'parent_id' => 'non_circular:locations,id',
'parent_id' => 'nullable|exists:locations,id|non_circular:locations,id',
];
protected $casts = [

View file

@ -74,7 +74,10 @@ class Setting extends Model
'login_remote_user_header_name' => 'string|nullable',
'thumbnail_max_h' => 'numeric|max:500|min:25',
'pwd_secure_min' => 'numeric|required|min:8',
'alert_threshold' => 'numeric|nullable',
'alert_interval' => 'numeric|nullable',
'audit_warning_days' => 'numeric|nullable',
'due_checkin_days' => 'numeric|nullable',
'audit_interval' => 'numeric|nullable',
'custom_forgot_pass_url' => 'url|nullable',
'privacy_policy_link' => 'nullable|url',

View file

@ -21,6 +21,11 @@ class SnipeModel extends Model
*/
public function setPurchaseCostAttribute($value)
{
if (is_float($value)) {
//value is *already* a floating-point number. Just assign it directly
$this->attributes['purchase_cost'] = $value;
return;
}
$value = Helper::ParseCurrency($value);
if ($value == 0) {

View file

@ -579,6 +579,6 @@ class AssetPresenter extends Presenter
public function glyph()
{
return '<i class="fas fa-barcode" aria-hidden="true"></i>';
return '<x-icon type="assets" />';
}
}

View file

@ -65,40 +65,46 @@ class CompanyPresenter extends Presenter
'field' => 'users_count',
'searchable' => false,
'sortable' => true,
'title' => '<span class="hidden-xs"><i class="fas fa-users"></i></span><span class="hidden-md hidden-lg">'.trans('general.users').'</span></th>',
'title' => trans('general.users'),
'visible' => true,
'class' => 'css-users',
], [
'field' => 'assets_count',
'searchable' => false,
'sortable' => true,
'title' => '<span class="hidden-xs"><i class="fas fa-barcode" aria-hidden="true"></i></span><span class="hidden-md hidden-lg">'.trans('general.assets').'</span>',
'title' => trans('general.assets'),
'visible' => true,
'class' => 'css-barcode',
], [
'field' => 'licenses_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.licenses'),
'visible' => true,
'title' => ' <span class="hidden-xs"><i class="far fa-save"></i></span><span class="hidden-md hidden-lg">'.trans('general.licenses').'</span>',
'class' => 'css-license',
], [
'field' => 'accessories_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.accessories'),
'visible' => true,
'title' => ' <span class="hidden-xs"><i class="far fa-keyboard"></i></span><span class="hidden-md hidden-lg">'.trans('general.accessories').'</span>',
'class' => 'css-accessory',
], [
'field' => 'consumables_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.consumables'),
'visible' => true,
'title' => ' <span class="hidden-xs"><i class="fas fa-tint"></i></span><span class="hidden-md hidden-lg">'.trans('general.consumables').'</span>',
'class' => 'css-consumable',
], [
'field' => 'components_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.components'),
'visible' => true,
'title' => ' <span class="hidden-xs"><i class="far fa-hdd"></i></span><span class="hidden-md hidden-lg">'.trans('general.components').'</span>',
'class' => 'css-component',
], [
'field' => 'updated_at',
'searchable' => false,

View file

@ -75,13 +75,13 @@ class ConsumablePresenter extends Presenter
], [
'field' => 'qty',
'searchable' => false,
'sortable' => false,
'sortable' => true,
'title' => trans('admin/components/general.total'),
'visible' => true,
], [
'field' => 'remaining',
'searchable' => false,
'sortable' => false,
'sortable' => true,
'title' => trans('admin/components/general.remaining'),
'visible' => true,
], [

View file

@ -46,6 +46,26 @@ class DepreciationPresenter extends Presenter
"title" => trans('admin/depreciations/table.depreciation_min'),
"visible" => true,
],
[
'field' => 'assets_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.assets'),
'visible' => true,
],
[
'field' => 'models_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.asset_models'),
'visible' => true,
], [
'field' => 'licenses_count',
'searchable' => false,
'sortable' => true,
'title' => trans('general.licenses'),
'visible' => true,
],
[
'field' => 'actions',
'searchable' => false,

View file

@ -394,6 +394,6 @@ class DepreciationReportPresenter extends Presenter
public function glyph()
{
return '<i class="fas fa-barcode" aria-hidden="true"></i>';
return '<x-icon type="reports" class="text-orange" />';
}
}

View file

@ -235,7 +235,7 @@ class LocationPresenter extends Presenter
public function glyph()
{
return '<i class="fas fa-map-marker-alt" aria-hidden="true"></i>';
return '<x-icon type="locations" />';
}
public function fullName()

View file

@ -94,36 +94,36 @@ class ManufacturerPresenter extends Presenter
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => ' <span class="hidden-md hidden-lg">Assets</span>'
.'<span class="hidden-xs"><i class="fas fa-barcode fa-lg"></i></span>',
'title' => trans('general.assets'),
'visible' => true,
'class' => 'css-barcode',
],
[
'field' => 'licenses_count',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => ' <span class="hidden-md hidden-lg">Licenses</span>'
.'<span class="hidden-xs"><i class="far fa-save fa-lg"></i></span>',
'title' => trans('general.licenses'),
'visible' => true,
'class' => 'css-license',
],
[
'field' => 'consumables_count',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => ' <span class="hidden-md hidden-lg">Consumables</span>'
.'<span class="hidden-xs"><i class="fas fa-tint fa-lg"></i></span>',
'title' => trans('general.consumables'),
'visible' => true,
'class' => 'css-consumable',
],
[
'field' => 'accessories_count',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => ' <span class="hidden-md hidden-lg">Accessories</span>'
.'<span class="hidden-xs"><i class="far fa-keyboard fa-lg"></i></span>',
'title' => trans('general.accessories'),
'visible' => true,
'class' => 'css-accessory',
],
[
'field' => 'created_at',

View file

@ -492,6 +492,6 @@ class UserPresenter extends Presenter
public function glyph()
{
return '<i class="fas fa-user" aria-hidden="true"></i>';
return '<x-icon type="user"/>';
}
}

View file

@ -6,10 +6,7 @@ use App\Models\CustomField;
use App\Models\Department;
use App\Models\Setting;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Validator;
/**
@ -91,18 +88,26 @@ class ValidationServiceProvider extends ServiceProvider
*
* $parameters[0] - the name of the first table we're looking at
* $parameters[1] - the ID (this will be 0 on new creations)
* $parameters[2] - the name of the second table we're looking at
* $parameters[2] - the name of the second field we're looking at
* $parameters[3] - the value that the request is passing for the second table we're
* checking for uniqueness across
*
*/
Validator::extend('two_column_unique_undeleted', function ($attribute, $value, $parameters, $validator) {
if (count($parameters)) {
$count = DB::table($parameters[0])
->select('id')->where($attribute, '=', $value)
->whereNull('deleted_at')
->where('id', '!=', $parameters[1])
->where($parameters[2], $parameters[3])->count();
->select('id')
->where($attribute, '=', $value)
->where('id', '!=', $parameters[1]);
if ($parameters[3]!='') {
$count = $count->where($parameters[2], $parameters[3]);
}
$count = $count->whereNull('deleted_at')
->count();
return $count < 1;
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Rules;
use App\Models\Setting;
use App\Models\User;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class UserCannotSwitchCompaniesIfItemsAssigned implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$user = User::find(request()->route('user')->id);
if (($value) && ($user->allAssignedCount() > 0) && (Setting::getSettings()->full_multiple_companies_support)) {
$fail(trans('admin/users/message.error.multi_company_items_assigned'));
}
}
}

View file

@ -78,7 +78,7 @@
"fakerphp/faker": "^1.16",
"larastan/larastan": "^2.9",
"mockery/mockery": "^1.4",
"nunomaduro/phpinsights": "^2.7",
"nunomaduro/phpinsights": "^2.11",
"php-mock/php-mock-phpunit": "^2.10",
"phpunit/phpunit": "^10.0",
"squizlabs/php_codesniffer": "^3.5",
@ -120,7 +120,9 @@
],
"post-create-project-cmd": [
"php artisan key:generate"
]
],
"coverage:herd:clover": "herd coverage vendor/bin/phpunit --coverage-clover tests/coverage/clover.xml",
"coverage:herd:html": "herd coverage vendor/bin/phpunit --coverage-html tests/coverage/html"
},
"config": {
"preferred-install": "dist",

2
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "35c741a2d3300848d758b187554b5b17",
"content-hash": "3819ab4ef72eb77fabe494c0e746b83b",
"packages": [
{
"name": "alek13/slack",

View file

@ -372,7 +372,9 @@ return [
'Google2FA' => PragmaRX\Google2FALaravel\Facade::class,
'Image' => Intervention\Image\ImageServiceProvider::class,
'Carbon' => Carbon\Carbon::class,
'Helper' => App\Helpers\Helper::class, // makes it much easier to use 'Helper::blah' in blades (which is where we usually use this)
'Helper' => App\Helpers\Helper::class,
// makes it much easier to use 'Helper::blah' in blades (which is where we usually use this)
'Icon' => App\Helpers\IconHelper::class,
'Socialite' => Laravel\Socialite\Facades\Socialite::class,

View file

@ -237,4 +237,6 @@ return [
],
],
'sanitize_by_default' => env('DB_SANITIZE_BY_DEFAULT', false),
];

View file

@ -1,10 +1,10 @@
<?php
return array (
'app_version' => 'v7.0.10',
'full_app_version' => 'v7.0.10 - build 14684-gc2bcc2e2d',
'build_version' => '14684',
'app_version' => 'v7.0.11',
'full_app_version' => 'v7.0.11 - build 15044-g46ed07642',
'build_version' => '15044',
'prerelease_version' => '',
'hash_version' => 'gc2bcc2e2d',
'full_hash' => 'v7.0.10-311-gc2bcc2e2d',
'branch' => 'master',
'hash_version' => 'g46ed07642',
'full_hash' => 'v7.0.11-133-g46ed07642',
'branch' => 'develop',
);

View file

@ -296,6 +296,11 @@ class UserFactory extends Factory
return $this->appendPermission(['reports.view' => '1']);
}
public function canImport()
{
return $this->appendPermission(['import' => '1']);
}
private function appendPermission(array $permission)
{
return $this->state(function ($currentState) use ($permission) {

View file

@ -0,0 +1,28 @@
<?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('users', function (Blueprint $table) {
$table->boolean('enable_confetti')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('enable_confetti');
});
}
};

View file

@ -0,0 +1,28 @@
<?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->integer('due_checkin_days')->nullable()->default(null);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('settings', function (Blueprint $table) {
$table->dropColumn('due_checkin_days');
});
}
};

View file

@ -1,3 +1,5 @@
# Compose file to spin up a local Snipe-IT for development.
version: '3'
services:
@ -8,43 +10,39 @@ services:
container_name: snipeit
ports:
- "8000:80"
volumes:
- ./storage/logs:/var/www/html/storage/logs
depends_on:
- mariadb
- redis
redis:
# The default needs to be stated.
condition: service_started
mariadb:
condition: service_healthy
restart: true
env_file:
- .env.docker
networks:
- snipeit-backend
- .env.dev.docker
mariadb:
image: mariadb:10.6.4-focal
image: mariadb:11.5.2
volumes:
- db:/var/lib/mysql
env_file:
- .env.docker
networks:
- snipeit-backend
- .env.dev.docker
ports:
- "3306:3306"
healthcheck:
# https://mariadb.com/kb/en/using-healthcheck-sh/#compose-file-example
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 2s
retries: 5
redis:
image: redis:6.2.5-buster
networks:
- snipeit-backend
image: redis:7.4.0
mailhog:
image: mailhog/mailhog:v1.0.1
ports:
# - 1025:1025
- "8025:8025"
networks:
- snipeit-backend
volumes:
db: {}
networks:
snipeit-backend: {}

View file

@ -1,11 +1,13 @@
# Compose file for production.
volumes:
db_data:
storage:
services:
app:
image: snipe/snipe-it:${APP_VERSION:-v6.4.1}
restart: always
image: snipe/snipe-it:${APP_VERSION:-v7.0.11}
restart: unless-stopped
volumes:
- storage:/var/lib/snipeit
ports:
@ -18,8 +20,8 @@ services:
- .env
db:
image: mariadb:10.6.4-focal
restart: always
image: mariadb:11.5.2
restart: unless-stopped
volumes:
- db_data:/var/lib/mysql
environment:
@ -28,7 +30,8 @@ services:
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
# https://mariadb.com/kb/en/using-healthcheck-sh/#compose-file-example
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 1s
retries: 5

View file

@ -1,7 +1,48 @@
#!/bin/bash
# Cribbed from nextcloud docker official repo
# https://github.com/nextcloud/docker/blob/master/docker-entrypoint.sh
# usage: file_env VAR [DEFAULT]
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
local varValue=$(env | grep -E "^${var}=" | sed -E -e "s/^${var}=//")
local fileVarValue=$(env | grep -E "^${fileVar}=" | sed -E -e "s/^${fileVar}=//")
if [ -n "${varValue}" ] && [ -n "${fileVarValue}" ]; then
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
exit 1
fi
if [ -n "${varValue}" ]; then
export "$var"="${varValue}"
elif [ -n "${fileVarValue}" ]; then
export "$var"="$(cat "${fileVarValue}")"
elif [ -n "${def}" ]; then
export "$var"="$def"
fi
unset "$fileVar"
}
# Add docker secrets support for the variables below:
file_env APP_KEY
file_env DB_HOST
file_env DB_PORT
file_env DB_DATABASE
file_env DB_USERNAME
file_env DB_PASSWORD
file_env REDIS_HOST
file_env REDIS_PASSWORD
file_env REDIS_PORT
file_env MAIL_HOST
file_env MAIL_PORT
file_env MAIL_USERNAME
file_env MAIL_PASSWORD
# fix key if needed
if [ -z "$APP_KEY" ]
if [ -z "$APP_KEY" -a -z "$APP_KEY_FILE" ]
then
echo "Please re-run this container with an environment variable \$APP_KEY"
echo "An example APP_KEY you could use is: "

View file

@ -1,7 +1,48 @@
#!/bin/sh
# Cribbed from nextcloud docker official repo
# https://github.com/nextcloud/docker/blob/master/docker-entrypoint.sh
# usage: file_env VAR [DEFAULT]
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
local varValue=$(env | grep -E "^${var}=" | sed -E -e "s/^${var}=//")
local fileVarValue=$(env | grep -E "^${fileVar}=" | sed -E -e "s/^${fileVar}=//")
if [ -n "${varValue}" ] && [ -n "${fileVarValue}" ]; then
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
exit 1
fi
if [ -n "${varValue}" ]; then
export "$var"="${varValue}"
elif [ -n "${fileVarValue}" ]; then
export "$var"="$(cat "${fileVarValue}")"
elif [ -n "${def}" ]; then
export "$var"="$def"
fi
unset "$fileVar"
}
# Add docker secrets support for the variables below:
file_env APP_KEY
file_env DB_HOST
file_env DB_PORT
file_env DB_DATABASE
file_env DB_USERNAME
file_env DB_PASSWORD
file_env REDIS_HOST
file_env REDIS_PASSWORD
file_env REDIS_PORT
file_env MAIL_HOST
file_env MAIL_PORT
file_env MAIL_USERNAME
file_env MAIL_PASSWORD
# fix key if needed
if [ -z "$APP_KEY" ]
if [ -z "$APP_KEY" -a -z "$APP_KEY_FILE" ]
then
echo "Please re-run this container with an environment variable \$APP_KEY"
echo "An example APP_KEY you could use is: "

63
package-lock.json generated
View file

@ -15,14 +15,15 @@
"bootstrap-colorpicker": "^2.5.3",
"bootstrap-datepicker": "^1.10.0",
"bootstrap-less": "^3.3.8",
"bootstrap-table": "1.23.0",
"bootstrap-table": "1.23.2",
"canvas-confetti": "^1.9.3",
"chart.js": "^2.9.4",
"clipboard": "^2.0.11",
"css-loader": "^5.0.0",
"ekko-lightbox": "^5.1.1",
"imagemin": "^8.0.1",
"jquery-slimscroll": "^1.3.8",
"jquery-ui": "^1.13.3",
"jquery-ui": "^1.14.0",
"jquery-validation": "^1.21.0",
"jquery.iframe-transport": "^1.0.0",
"jspdf-autotable": "^3.8.2",
@ -36,7 +37,7 @@
"signature_pad": "^4.2.0",
"tableexport.jquery.plugin": "1.30.0",
"tether": "^1.4.0",
"webpack": "^5.92.0"
"webpack": "^5.94.0"
},
"devDependencies": {
"all-contributors-cli": "^6.26.1",
@ -2104,25 +2105,10 @@
"@types/node": "*"
}
},
"node_modules/@types/eslint": {
"version": "8.56.10",
"license": "MIT",
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@types/eslint-scope": {
"version": "3.7.7",
"license": "MIT",
"dependencies": {
"@types/eslint": "*",
"@types/estree": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.5",
"license": "MIT"
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@types/express": {
"version": "4.17.21",
@ -3692,9 +3678,9 @@
"license": "MIT"
},
"node_modules/bootstrap-table": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.23.0.tgz",
"integrity": "sha512-fAIhu2CAqMsZWkzeFxXyh0yQA2DMBdB0tCdr1iF6bKr3c/Hf79cw5PykNt7NdtqLz/a0p192S8EKyT5lG4yrpw==",
"version": "1.23.2",
"resolved": "https://registry.npmjs.org/bootstrap-table/-/bootstrap-table-1.23.2.tgz",
"integrity": "sha512-1IFiWFZzbKlleXgYEHdwHkX6rxlQMEx2N1tA8rJK/j08pI+NjIGnxFeXUL26yQLQ0U135eis/BX3OV1+anY25g==",
"peerDependencies": {
"jquery": "3"
}
@ -4098,6 +4084,15 @@
],
"license": "CC-BY-4.0"
},
"node_modules/canvas-confetti": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.3.tgz",
"integrity": "sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==",
"funding": {
"type": "donate",
"url": "https://www.paypal.me/kirilvatev"
}
},
"node_modules/canvg": {
"version": "3.0.10",
"license": "MIT",
@ -5300,9 +5295,9 @@
}
},
"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.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@ -7057,10 +7052,11 @@
"license": "BSD-2-Clause"
},
"node_modules/jquery-ui": {
"version": "1.13.3",
"license": "MIT",
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.14.0.tgz",
"integrity": "sha512-mPfYKBoRCf0MzaT2cyW5i3IuZ7PfTITaasO5OFLAQxrHuI+ZxruPa+4/K1OMNT8oElLWGtIxc9aRbyw20BKr8g==",
"dependencies": {
"jquery": ">=1.8.0 <4.0.0"
"jquery": ">=1.12.0 <5.0.0"
}
},
"node_modules/jquery-validation": {
@ -10869,11 +10865,10 @@
"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.94.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"dependencies": {
"@types/eslint-scope": "^3.7.3",
"@types/estree": "^1.0.5",
"@webassemblyjs/ast": "^1.12.1",
"@webassemblyjs/wasm-edit": "^1.12.1",
@ -10882,7 +10877,7 @@
"acorn-import-attributes": "^1.9.5",
"browserslist": "^4.21.10",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.0",
"enhanced-resolve": "^5.17.1",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",

View file

@ -35,14 +35,15 @@
"bootstrap-colorpicker": "^2.5.3",
"bootstrap-datepicker": "^1.10.0",
"bootstrap-less": "^3.3.8",
"bootstrap-table": "1.23.0",
"bootstrap-table": "1.23.2",
"canvas-confetti": "^1.9.3",
"chart.js": "^2.9.4",
"clipboard": "^2.0.11",
"css-loader": "^5.0.0",
"ekko-lightbox": "^5.1.1",
"imagemin": "^8.0.1",
"jquery-slimscroll": "^1.3.8",
"jquery-ui": "^1.13.3",
"jquery-ui": "^1.14.0",
"jquery-validation": "^1.21.0",
"jquery.iframe-transport": "^1.0.0",
"jspdf-autotable": "^3.8.2",
@ -56,6 +57,6 @@
"signature_pad": "^4.2.0",
"tableexport.jquery.plugin": "1.30.0",
"tether": "^1.4.0",
"webpack": "^5.92.0"
"webpack": "^5.94.0"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/dist/all.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,9 +1,9 @@
{
"/js/build/app.js": "/js/build/app.js?id=da3f7fee4a180ba924f6a3920c94eb71",
"/js/build/app.js": "/js/build/app.js?id=5e9ac5c1a7e089f056fb1dba566193a6",
"/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=f0b08873a06bb54daeee176a9459f4a9",
"/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=f4397c717b99fce41a633ca6edd5d1f4",
"/css/build/overrides.css": "/css/build/overrides.css?id=a759aa24710e294392877c5139fda40e",
"/css/build/app.css": "/css/build/app.css?id=b3b3df70f679f45e15a6bcd28a8e87cc",
"/css/build/overrides.css": "/css/build/overrides.css?id=ebd921b0b5dca37487551bcc7dc934c5",
"/css/build/app.css": "/css/build/app.css?id=2b1b6164d02342fcd4cd303fef52e895",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=4ea0068716c1bb2434d87a16d51b98c9",
"/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=7b315b9612b8fde8f9c5b0ddb6bba690",
"/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=393aaa7b368b0670fc42434c8cca7dc7",
@ -19,7 +19,7 @@
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=f677207c6cf9678eb539abecb408c374",
"/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=0640e45bad692dcf62873c6e85904899",
"/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=76482123f6c70e866d6b971ba91de7bb",
"/css/dist/all.css": "/css/dist/all.css?id=7b8e04041af3dfe3de25d73107bfda91",
"/css/dist/all.css": "/css/dist/all.css?id=c1cd73524bd82ddb8a4d7e8d1a504506",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7",
"/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde",
@ -90,8 +90,8 @@
"/css/webfonts/fa-solid-900.woff2": "/css/webfonts/fa-solid-900.woff2?id=541cafc702f56f57de95f3d1f792f428",
"/css/webfonts/fa-v4compatibility.ttf": "/css/webfonts/fa-v4compatibility.ttf?id=51ade19e1b10d7a0031b18568a2b01d5",
"/css/webfonts/fa-v4compatibility.woff2": "/css/webfonts/fa-v4compatibility.woff2?id=1cc408d68a27c3757b4460bbc542433e",
"/js/dist/bootstrap-table-locale-all.min.js": "/js/dist/bootstrap-table-locale-all.min.js?id=27eb00f47f9bae70cd630d184b7969f1",
"/js/dist/bootstrap-table-en-US.min.js": "/js/dist/bootstrap-table-en-US.min.js?id=57bdb4770b2924f5efeda100caf3c9b7",
"/js/dist/bootstrap-table-locale-all.min.js": "/js/dist/bootstrap-table-locale-all.min.js?id=467938d6a524df8e62c4fb8ae5e7f3f1",
"/js/dist/bootstrap-table-en-US.min.js": "/js/dist/bootstrap-table-en-US.min.js?id=d4ef3db8dc9f809258218c187de5ee2a",
"/css/dist/skins/_all-skins.min.css": "/css/dist/skins/_all-skins.min.css?id=f4397c717b99fce41a633ca6edd5d1f4",
"/css/dist/skins/skin-black-dark.min.css": "/css/dist/skins/skin-black-dark.min.css?id=f0b08873a06bb54daeee176a9459f4a9",
"/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=76482123f6c70e866d6b971ba91de7bb",
@ -108,8 +108,8 @@
"/css/dist/skins/skin-red.min.css": "/css/dist/skins/skin-red.min.css?id=44bf834f2110504a793dadec132a5898",
"/css/dist/skins/skin-yellow-dark.min.css": "/css/dist/skins/skin-yellow-dark.min.css?id=393aaa7b368b0670fc42434c8cca7dc7",
"/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=7b315b9612b8fde8f9c5b0ddb6bba690",
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=8abbb6aea625ec64cd7ebdad77ebf6e5",
"/js/build/vendor.js": "/js/build/vendor.js?id=c1c24b883f48dc3d16b817aed0b457cc",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=859e11e4e6b05c84e4b7302de29bac5e",
"/js/dist/all.js": "/js/dist/all.js?id=ea6fb4f01f01c2194310403dafc1658f"
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=393d720a0f9aba560094fbc8d3b0c0f0",
"/js/build/vendor.js": "/js/build/vendor.js?id=5269eb5a6beb74f03387c78938cf17b2",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=6660df122e24940d42d03c06775fec7b",
"/js/dist/all.js": "/js/dist/all.js?id=e0a4b1a80b09333a460973137f39eab4"
}

BIN
public/sounds/lock.mp3 Normal file

Binary file not shown.

View file

@ -82,47 +82,31 @@ pieOptions = {
var baseUrl = $('meta[name="baseUrl"]').attr('content');
(function($, settings) {
var Components = {};
Components.modals = {};
$(function () {
// confirm restore modal
Components.modals.confirmRestore = function() {
var $el = $('table');
var events = {
'click': function(evnt) {
// confirm restore modal
$el.on('click', '.restore-asset', function (evnt) {
var $context = $(this);
var $restoreConfirmModal = $('#restoreConfirmModal');
var href = $context.attr('href');
var message = $context.attr('data-content');
var title = $context.attr('data-title');
$('#restoreConfirmModalLabel').text(title);
$('#confirmModalLabel').text(title);
$restoreConfirmModal.find('.modal-body').text(message);
$('#restoreForm').attr('action', href);
$restoreConfirmModal.modal({
show: true
});
return false;
}
};
var render = function() {
$el.on('click', '.restore-asset', events['click']);
};
return {
render: render
};
};
});
// confirm delete modal
Components.modals.confirmDelete = function() {
var $el = $('table');
var events = {
'click': function(evnt) {
$el.on('click', '.delete-asset', function (evnt) {
var $context = $(this);
var $dataConfirmModal = $('#dataConfirmModal');
var href = $context.attr('href');
@ -136,30 +120,7 @@ var baseUrl = $('meta[name="baseUrl"]').attr('content');
show: true
});
return false;
}
};
var render = function() {
$el.on('click', '.delete-asset', events['click']);
};
return {
render: render
};
};
/**
* Application start point
* Component definition stays out of load event, execution only happens.
*/
$(function() {
new Components.modals.confirmRestore().render();
new Components.modals.confirmDelete().render();
});
}(jQuery, window.snipeit.settings));
$(document).ready(function () {
/*
* Slideout help menu

View file

@ -358,6 +358,10 @@ body {
white-space: normal;
}
.modal-warning .modal-help {
color: #fff8af;
}
.bootstrap-table .fixed-table-container .fixed-table-body .fixed-table-loading {
z-index: 0 !important;
}
@ -496,13 +500,27 @@ body {
.select2-selection--multiple {
border-color: #d2d6de !important;
height: 34px;
overflow-y: auto;
}
.select2-selection__choice {
border-radius: 0px !important;
}
.select2-search select2-search--inline {
height: 35px !important;
float: left;
margin: 0;
}
.select2-results__option {
padding: 5px;
user-select: none;
-webkit-user-select: none;
margin: 0px;
}
img.navbar-brand-img, .navbar-brand>img {
float: left;
@ -510,8 +528,8 @@ img.navbar-brand-img, .navbar-brand>img {
max-height: 50px;
}
.input-daterange {
border-radius: 0px;
.input-daterange, .input-daterange input:first-child, .input-daterange input:last-child {
border-radius: 0px !important;
}
.btn.bg-maroon, .btn.bg-purple{
@ -718,6 +736,7 @@ th.css-consumable > .th-inner,
th.css-envelope > .th-inner,
th.css-users > .th-inner,
th.css-location > .th-inner,
th.css-component > .th-inner,
th.css-accessory > .th-inner
{
font-size: 0px;
@ -736,6 +755,7 @@ th.css-consumable > .th-inner::before,
th.css-envelope > .th-inner::before,
th.css-users > .th-inner::before,
th.css-location > .th-inner::before,
th.css-component > .th-inner::before,
th.css-accessory > .th-inner::before
{
@ -791,6 +811,11 @@ th.css-location > .th-inner::before {
content: "\f3c5"; font-family: "Font Awesome 5 Free"; font-size: 19px; margin-bottom: 0px;
}
th.css-component > .th-inner::before
{
content: "\f0a0"; font-family: "Font Awesome 5 Free"; font-weight: 500;
}
.small-box .inner {
padding-left: 15px;
@ -844,19 +869,39 @@ th.css-location > .th-inner::before {
margin-top:50px
}
}
@media screen and (max-width: 992px){
.info-stack-container {
display: flex;
flex-direction: column;
}
.col-md-3.col-xs-12.col-sm-push-9.info-stack{
left:auto;
order:1;
}
.col-md-9.col-xs-12.col-sm-pull-3.info-stack{
right:auto;
order:2;
}
.info-stack-container > .col-md-9.col-xs-12.col-sm-pull-3.info-stack > .row-new-striped > .row > .col-sm-2{
width:auto;
float:none;
}
}
@media screen and (max-width: 1318px) and (min-width: 1200px){
.box{
.admin.box{
height:170px;
}
}
.ellipsis {
overflow: hidden;
@media screen and (max-width: 1494px) and (min-width: 1200px){
.dashboard.small-box{
white-space: nowrap;
text-overflow: ellipsis;
max-width: 188px;
display: block;
overflow: hidden;
}
}
/** Form-stuff overrides for checkboxes and stuff **/
label.form-control {

View file

@ -12,4 +12,6 @@ return array(
'api_reference' => 'crwdns12270:0crwdne12270:0',
'profile_updated' => 'crwdns12202:0crwdne12202:0',
'no_tokens' => 'crwdns12318:0crwdne12318:0',
'enable_sounds' => 'crwdns12658:0crwdne12658:0',
'enable_confetti' => 'crwdns12664:0crwdne12664:0',
);

View file

@ -58,6 +58,7 @@ return [
'file_delete_success' => 'crwdns1698:0crwdne1698:0',
'file_delete_error' => 'crwdns1699:0crwdne1699:0',
'file_missing' => 'crwdns11835:0crwdne11835:0',
'file_already_deleted' => 'crwdns12694:0crwdne12694:0',
'header_row_has_malformed_characters' => 'crwdns11229:0crwdne11229:0',
'content_row_has_malformed_characters' => 'crwdns11231:0crwdne11231:0',
],

View file

@ -47,4 +47,5 @@ return [
'kit_deleted' => 'crwdns6659:0crwdne6659:0',
'kit_model_updated' => 'crwdns6661:0crwdne6661:0',
'kit_model_detached' => 'crwdns6663:0crwdne6663:0',
'model_already_attached' => 'crwdns12684:0crwdne12684:0',
];

View file

@ -3,11 +3,12 @@
return array(
'does_not_exist' => 'crwdns650:0crwdne650:0',
'assoc_users' => 'crwdns12272:0crwdne12272:0',
'assoc_users' => 'crwdns12666:0crwdne12666:0',
'assoc_assets' => 'crwdns1404:0crwdne1404:0',
'assoc_child_loc' => 'crwdns1405:0crwdne1405:0',
'assigned_assets' => 'crwdns11179:0crwdne11179:0',
'current_location' => 'crwdns11181:0crwdne11181:0',
'open_map' => 'crwdns12696:0crwdne12696:0',
'create' => array(
@ -20,6 +21,11 @@ return array(
'success' => 'crwdns655:0crwdne655:0'
),
'restore' => array(
'error' => 'crwdns12698:0crwdne12698:0',
'success' => 'crwdns12700:0crwdne12700:0'
),
'delete' => array(
'confirm' => 'crwdns656:0crwdne656:0',
'error' => 'crwdns657:0crwdne657:0',

View file

@ -7,7 +7,7 @@ return array(
'no_association' => 'crwdns11693:0crwdne11693:0',
'no_association_fix' => 'crwdns11235:0crwdne11235:0',
'assoc_users' => 'crwdns672:0crwdne672:0',
'invalid_category_type' => 'crwdns12302:0crwdne12302:0',
'invalid_category_type' => 'crwdns12678:0crwdne12678:0',
'create' => array(
'error' => 'crwdns673:0crwdne673:0',

View file

@ -218,6 +218,8 @@ return [
'webhook_integration_help' => 'crwdns11403:0crwdne11403:0',
'webhook_integration_help_button' => 'crwdns11405:0crwdne11405:0',
'webhook_test_help' => 'crwdns11407:0crwdne11407:0',
'shortcuts_enabled' => 'crwdns12654:0crwdne12654:0',
'shortcuts_help_text' => 'crwdns12656:0crwdne12656:0',
'snipe_version' => 'crwdns1266:0crwdne1266:0',
'support_footer' => 'crwdns1991:0crwdne1991:0',
'support_footer_help' => 'crwdns1992:0crwdne1992:0',
@ -379,5 +381,7 @@ return [
'default_avatar_help' => 'crwdns12590:0crwdne12590:0',
'restore_default_avatar' => 'crwdns12592:0crwdne12592:0',
'restore_default_avatar_help' => 'crwdns12594:0crwdne12594:0',
'due_checkin_days' => 'crwdns12680:0crwdne12680:0',
'due_checkin_days_help' => 'crwdns12682:0crwdne12682:0',
];

View file

@ -14,6 +14,9 @@ return [
'restore_warning' => 'crwdns6709:0crwdne6709:0',
'restore_confirm' => 'crwdns6711:0crwdne6711:0'
],
'restore' => [
'success' => 'crwdns12668:0crwdne12668:0'
],
'purge' => [
'error' => 'crwdns1615:0crwdne1615:0',
'validation_failed' => 'crwdns1616:0crwdne1616:0',

View file

@ -26,7 +26,7 @@ return [
'clone' => 'crwdns12598:0crwdne12598:0',
'edit' => 'crwdns12600:0crwdne12600:0',
'delete' => 'crwdns12602:0crwdne12602:0',
'restore' => 'crwdns12604:0crwdne12604:0',
'restore' => 'crwdns12646:0crwdne12646:0',
'create' => 'crwdns12606:0crwdne12606:0',
'checkout' => 'crwdns12608:0crwdne12608:0',
'checkin' => 'crwdns12610:0crwdne12610:0',

View file

@ -77,7 +77,7 @@ return [
'consumables' => 'crwdns1327:0crwdne1327:0',
'country' => 'crwdns1039:0crwdne1039:0',
'could_not_restore' => 'crwdns11894:0:item_type:crwdne11894:0',
'not_deleted' => 'crwdns11896:0crwdne11896:0',
'not_deleted' => 'crwdns12670:0crwdne12670:0',
'create' => 'crwdns1040:0crwdne1040:0',
'created' => 'crwdns1773:0crwdne1773:0',
'created_asset' => 'crwdns1041:0crwdne1041:0',
@ -98,7 +98,7 @@ return [
'debug_warning_text' => 'crwdns1828:0crwdne1828:0',
'delete' => 'crwdns1046:0crwdne1046:0',
'delete_confirm' => 'crwdns2020:0crwdne2020:0',
'delete_confirm_no_undo' => 'crwdns11599:0crwdne11599:0',
'delete_confirm_no_undo' => 'crwdns12672:0crwdne12672:0',
'deleted' => 'crwdns1047:0crwdne1047:0',
'delete_seats' => 'crwdns1430:0crwdne1430:0',
'deletion_failed' => 'crwdns6117:0crwdne6117:0',
@ -134,7 +134,7 @@ return [
'lastname_firstinitial' => 'crwdns5952:0crwdne5952:0',
'firstinitial.lastname' => 'crwdns5954:0crwdne5954:0',
'firstnamelastinitial' => 'crwdns5956:0crwdne5956:0',
'lastnamefirstname' => 'crwdns12266:0crwdne12266:0',
'lastnamefirstname' => 'crwdns12620:0crwdne12620:0',
'first_name' => 'crwdns1053:0crwdne1053:0',
'first_name_format' => 'crwdns1647:0crwdne1647:0',
'files' => 'crwdns1996:0crwdne1996:0',
@ -156,9 +156,9 @@ return [
'image_delete' => 'crwdns1057:0crwdne1057:0',
'include_deleted' => 'crwdns10518:0crwdne10518:0',
'image_upload' => 'crwdns1058:0crwdne1058:0',
'filetypes_accepted_help' => 'crwdns6129:0crwdne6129:0',
'filetypes_size_help' => 'crwdns6131:0crwdne6131:0',
'image_filetypes_help' => 'crwdns12284:0crwdne12284:0',
'filetypes_accepted_help' => 'crwdns12622:0crwdne12622:0',
'filetypes_size_help' => 'crwdns12624:0crwdne12624:0',
'image_filetypes_help' => 'crwdns12674:0crwdne12674:0',
'unaccepted_image_type' => 'crwdns11365:0crwdne11365:0',
'import' => 'crwdns1411:0crwdne1411:0',
'import_this_file' => 'crwdns11922:0crwdne11922:0',
@ -183,7 +183,7 @@ return [
'licenses_available' => 'crwdns12172:0crwdne12172:0',
'licenses' => 'crwdns1062:0crwdne1062:0',
'list_all' => 'crwdns1063:0crwdne1063:0',
'loading' => 'crwdns6135:0crwdne6135:0',
'loading' => 'crwdns12628:0crwdne12628:0',
'lock_passwords' => 'crwdns5974:0crwdne5974:0',
'feature_disabled' => 'crwdns1774:0crwdne1774:0',
'location' => 'crwdns1064:0crwdne1064:0',
@ -193,7 +193,7 @@ return [
'logout' => 'crwdns1066:0crwdne1066:0',
'lookup_by_tag' => 'crwdns1648:0crwdne1648:0',
'maintenances' => 'crwdns1998:0crwdne1998:0',
'manage_api_keys' => 'crwdns6137:0crwdne6137:0',
'manage_api_keys' => 'crwdns12630:0crwdne12630:0',
'manufacturer' => 'crwdns1067:0crwdne1067:0',
'manufacturers' => 'crwdns1068:0crwdne1068:0',
'markdown' => 'crwdns1574:0crwdne1574:0',
@ -254,7 +254,7 @@ return [
'select_all' => 'crwdns6155:0crwdne6155:0',
'search' => 'crwdns1290:0crwdne1290:0',
'select_category' => 'crwdns1663:0crwdne1663:0',
'select_datasource' => 'crwdns12166:0crwdne12166:0',
'select_datasource' => 'crwdns12632:0crwdne12632:0',
'select_department' => 'crwdns1880:0crwdne1880:0',
'select_depreciation' => 'crwdns1282:0crwdne1282:0',
'select_location' => 'crwdns1283:0crwdne1283:0',
@ -274,11 +274,12 @@ return [
'signed_off_by' => 'crwdns6784:0crwdne6784:0',
'skin' => 'crwdns2002:0crwdne2002:0',
'webhook_msg_note' => 'crwdns11483:0crwdne11483:0',
'webhook_test_msg' => 'crwdns11371:0crwdne11371:0',
'webhook_test_msg' => 'crwdns12634:0crwdne12634:0',
'some_features_disabled' => 'crwdns1669:0crwdne1669:0',
'site_name' => 'crwdns1086:0crwdne1086:0',
'state' => 'crwdns1087:0crwdne1087:0',
'status_labels' => 'crwdns1088:0crwdne1088:0',
'status_label' => 'crwdns12662:0crwdne12662:0',
'status' => 'crwdns1089:0crwdne1089:0',
'accept_eula' => 'crwdns6786:0crwdne6786:0',
'supplier' => 'crwdns1833:0crwdne1833:0',
@ -339,16 +340,16 @@ return [
'view_all' => 'crwdns6165:0crwdne6165:0',
'hide_deleted' => 'crwdns6167:0crwdne6167:0',
'email' => 'crwdns6169:0crwdne6169:0',
'do_not_change' => 'crwdns6171:0crwdne6171:0',
'bug_report' => 'crwdns6173:0crwdne6173:0',
'do_not_change' => 'crwdns12636:0crwdne12636:0',
'bug_report' => 'crwdns12638:0crwdne12638:0',
'user_manual' => 'crwdns6175:0crwdne6175:0',
'setup_step_1' => 'crwdns6177:0crwdne6177:0',
'setup_step_2' => 'crwdns6179:0crwdne6179:0',
'setup_step_3' => 'crwdns6181:0crwdne6181:0',
'setup_step_4' => 'crwdns6183:0crwdne6183:0',
'setup_config_check' => 'crwdns6185:0crwdne6185:0',
'setup_create_database' => 'crwdns6187:0crwdne6187:0',
'setup_create_admin' => 'crwdns6189:0crwdne6189:0',
'setup_create_database' => 'crwdns12640:0crwdne12640:0',
'setup_create_admin' => 'crwdns12676:0crwdne12676:0',
'setup_done' => 'crwdns6191:0crwdne6191:0',
'bulk_edit_about_to' => 'crwdns6193:0crwdne6193:0',
'checked_out' => 'crwdns6195:0crwdne6195:0',
@ -424,7 +425,7 @@ return [
'assets_by_status_type' => 'crwdns10544:0crwdne10544:0',
'pie_chart_type' => 'crwdns10546:0crwdne10546:0',
'hello_name' => 'crwdns10548:0crwdne10548:0',
'unaccepted_profile_warning' => 'crwdns10550:0crwdne10550:0',
'unaccepted_profile_warning' => 'crwdns12686:0crwdne12686:0',
'start_date' => 'crwdns11168:0crwdne11168:0',
'end_date' => 'crwdns11170:0crwdne11170:0',
'alt_uploaded_image_thumbnail' => 'crwdns11172:0crwdne11172:0',
@ -557,5 +558,8 @@ return [
'expires' => 'crwdns12310:0crwdne12310:0',
'map_fields'=> 'crwdns12572:0crwdne12572:0',
'remaining_var' => 'crwdns12612:0crwdne12612:0',
'assets_in_var' => 'crwdns12688:0crwdne12688:0',
'label' => 'crwdns12690:0crwdne12690:0',
'import_asset_tag_exists' => 'crwdns12692:0crwdne12692:0',
];

View file

@ -40,7 +40,9 @@ return [
'ms-MY'=> 'crwdns11986:0crwdne11986:0',
'mi-NZ'=> 'crwdns11988:0crwdne11988:0',
'mn-MN'=> 'crwdns11990:0crwdne11990:0',
'no-NO'=> 'crwdns11992:0crwdne11992:0',
//'no-NO'=> 'Norwegian',
'nb-NO'=> 'crwdns12644:0crwdne12644:0',
//'nn-NO'=> 'Norwegian Nynorsk',
'fa-IR'=> 'crwdns11994:0crwdne11994:0',
'pl-PL'=> 'crwdns11996:0crwdne11996:0',
'pt-PT'=> 'crwdns10634:0crwdne10634:0',

View file

@ -125,6 +125,8 @@ return [
'symbols' => 'crwdns12504:0crwdne12504:0',
'uncompromised' => 'crwdns12506:0crwdne12506:0',
],
'percent' => 'crwdns12660:0crwdne12660:0',
'present' => 'crwdns1936:0crwdne1936:0',
'present_if' => 'crwdns12508:0crwdne12508:0',
'present_unless' => 'crwdns12510:0crwdne12510:0',
@ -188,6 +190,8 @@ return [
'hashed_pass' => 'crwdns1946:0crwdne1946:0',
'dumbpwd' => 'crwdns1947:0crwdne1947:0',
'statuslabel_type' => 'crwdns1948:0crwdne1948:0',
'custom_field_not_found' => 'crwdns12650:0crwdne12650:0',
'custom_field_not_found_on_model' => 'crwdns12652:0crwdne12652:0',
// date_format validation with slightly less stupid messages. It duplicates a lot, but it gets the job done :(
// We use this because the default error message for date_format is reflects php Y-m-d, which non-PHP

View file

@ -12,4 +12,6 @@ return array(
'api_reference' => 'Please check the <a href="https://snipe-it.readme.io/reference" target="_blank">API reference</a> to find specific API endpoints and additional API documentation.',
'profile_updated' => 'Account successfully updated',
'no_tokens' => 'You have not created any personal access tokens.',
'enable_sounds' => 'Enable sound effects',
'enable_confetti' => 'Enable confetti effects',
);

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