Merge pull request #10889 from uberbrady/scim_squashed_grok_library

SCIM integration using laravel-scim-server library
This commit is contained in:
snipe 2022-04-05 20:32:16 +01:00 committed by GitHub
commit 7385a0765e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 511 additions and 816 deletions

16
app/Models/SCIMUser.php Normal file
View file

@ -0,0 +1,16 @@
<?php
namespace App\Models;
class SCIMUser extends User
{
protected $table = 'users';
protected $throwValidationExceptions = true; // we want model-level validation to fully THROW, not just return false
public function __construct(array $attributes = []) {
$attributes['password'] = "*NO PASSWORD*";
// $attributes['activated'] = 1;
parent::__construct($attributes);
}
}

View file

@ -0,0 +1,174 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Helper;
use ArieTimmerman\Laravel\SCIMServer\SCIM\Schema;
use ArieTimmerman\Laravel\SCIMServer\Attribute\AttributeMapping;
class SnipeSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig
{
public function getUserConfig()
{
$config = parent::getUserConfig();
// Much of this is copied verbatim from the library, then adjusted for our needs
$config['class'] = SCIMUser::class;
unset($config['mapping']['example:name:space']);
$config['map_unmapped'] = false; // anything we don't explicitly map will _not_ show up.
$core_namespace = 'urn:ietf:params:scim:schemas:core:2.0:User';
$core = $core_namespace.':';
$mappings =& $config['mapping'][$core_namespace]; //grab this entire key, we don't want to be repeating ourselves
//username - *REQUIRED*
$config['validations'][$core.'userName'] = 'required';
$mappings['userName'] = AttributeMapping::eloquent('username');
//human name - *FIRST NAME REQUIRED*
$config['validations'][$core.'name.givenName'] = 'required';
$config['validations'][$core.'name.familyName'] = 'string'; //not required
$mappings['name']['familyName'] = AttributeMapping::eloquent("last_name");
$mappings['name']['givenName'] = AttributeMapping::eloquent("first_name");
$mappings['name']['formatted'] = (new AttributeMapping())->ignoreWrite()->setRead(
function (&$object) {
return $object->getFullNameAttribute();
}
);
$config['validations'][$core.'emails'] = 'nullable|array'; // emails are not required in Snipe-IT...
$config['validations'][$core.'emails.*.value'] = 'required|email'; // ...but if you give us one, it better be an email address
$mappings['emails'] = [[
"value" => AttributeMapping::eloquent("email"),
"display" => null,
"type" => AttributeMapping::constant("work")->ignoreWrite(),
"primary" => AttributeMapping::constant(true)->ignoreWrite()
]];
//active
$config['validations'][$core.'active'] = 'boolean';
$mappings['active'] = AttributeMapping::eloquent('activated');
//phone
$config['validations'][$core.'phoneNumbers'] = 'nullable|array';
$config['validations'][$core.'phoneNumbers.*.value'] = 'required';
$mappings['phoneNumbers'] = [[
"value" => AttributeMapping::eloquent("phone"),
"display" => null,
"type" => AttributeMapping::constant("work")->ignoreWrite(),
"primary" => AttributeMapping::constant(true)->ignoreWrite()
]];
//address
$config['validations'][$core.'addresses'] = 'nullable|array';
$config['validations'][$core.'addresses.*.streetAddress'] = 'required';
$config['validations'][$core.'addresses.*.locality'] = 'string';
$config['validations'][$core.'addresses.*.region'] = 'string';
$config['validations'][$core.'addresses.*.postalCode'] = 'string';
$config['validations'][$core.'addresses.*.country'] = 'string';
$mappings['addresses'] = [[
'type' => AttributeMapping::constant("work")->ignoreWrite(),
'formatted' => AttributeMapping::constant("n/a")->ignoreWrite(), // TODO - is this right? This doesn't look right.
'streetAddress' => AttributeMapping::eloquent("address"),
'locality' => AttributeMapping::eloquent("city"),
'region' => AttributeMapping::eloquent("state"),
'postalCode' => AttributeMapping::eloquent("zip"),
'country' => AttributeMapping::eloquent("country"),
'primary' => AttributeMapping::constant(true)->ignoreWrite() //this isn't in the example?
]];
//title
$config['validations'][$core.'title'] = 'string';
$mappings['title'] = AttributeMapping::eloquent('jobtitle');
//Preferred Language
$config['validations'][$core.'preferredLanguage'] = 'string';
$mappings['preferredLanguage'] = AttributeMapping::eloquent('locale');
/*
more snipe-it attributes I'd like to check out (to map to 'enterprise' maybe?):
- website
- notes?
- remote???
- location_id ?
- company_id to "organization?"
*/
$enterprise_namespace = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User';
$ent = $enterprise_namespace.':';
// we remove the 'example' namespace and add the Enterprise one
$config['mapping']['schemas'] = AttributeMapping::constant( [$core_namespace, $enterprise_namespace] )->ignoreWrite();
$config['validations'][$ent.'employeeNumber'] = 'string';
$config['validations'][$ent.'department'] = 'string';
$config['validations'][$ent.'manager'] = 'nullable';
$config['validations'][$ent.'manager.value'] = 'string';
$config['mapping'][$enterprise_namespace] = [
'employeeNumber' => AttributeMapping::eloquent('employee_num'),
'department' =>(new AttributeMapping())->setAdd( // FIXME parent?
function ($value, &$object) {
\Log::error("Department-Add: $value"); //FIXME
$department = Department::where("name", $value)->first();
if ($department) {
$object->department_id = $department->id;
}
}
)->setReplace(
function ($value, &$object) {
\Log::error("Department-Replace: $value"); //FIXME
$department = Department::where("name", $value)->first();
if ($department) {
$object->department_id = $department->id;
}
}
)->setRead(
function (&$object) {
\Log::error("Weird department reader firing..."); //FIXME
return $object->department ? $object->department->name : null;
}
),
'manager' => [
// FIXME - manager writes are disabled. This kinda works but it leaks errors all over the place. Not cool.
// '$ref' => (new AttributeMapping())->ignoreWrite()->ignoreRead(),
// 'displayName' => (new AttributeMapping())->ignoreWrite()->ignoreRead(),
// NOTE: you could probably do a 'plain' Eloquent mapping here, but we don't for future-proofing
'value' => (new AttributeMapping())->setAdd(
function ($value, &$object) {
\Log::error("Manager-Add: $value"); //FIXME
$manager = User::find($value);
if ($manager) {
$object->manager_id = $manager->id;
}
}
)->setReplace(
function ($value, &$object) {
\Log::error("Manager-Replace: $value"); //FIXME
$manager = User::find($value);
if ($manager) {
$object->manager_id = $manager->id;
}
}
)->setRead(
function (&$object) {
\Log::error("Weird manager reader firing..."); //FIXME
return $object->manager_id;
}
),
]
];
return $config;
}
}

View file

@ -8,6 +8,7 @@ use App\Models\Component;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Setting;
use App\Models\SnipeSCIMConfig;
use App\Observers\AccessoryObserver;
use App\Observers\AssetObserver;
use App\Observers\ComponentObserver;
@ -80,6 +81,8 @@ class AppServiceProvider extends ServiceProvider
if ($this->app->environment(['local', 'develop'])) {
$this->app->register(\Laravel\Dusk\DuskServiceProvider::class);
}
$this->app->singleton('ArieTimmerman\Laravel\SCIMServer\SCIMConfig', SnipeSCIMConfig::class); // this overrides the default SCIM configuration with our own
}
}

View file

@ -24,7 +24,7 @@ class RouteServiceProvider extends ServiceProvider
$this->mapWebRoutes();
//
require base_path('routes/scim.php');
});
}

View file

@ -10,6 +10,12 @@
],
"license": "AGPL-3.0-or-later",
"type": "project",
"repositories": [
{
"type": "vcs",
"url": "https://github.com/grokability/laravel-scim-server"
}
],
"require": {
"php": ">=7.4 <8.1",
"ext-curl": "*",
@ -18,6 +24,7 @@
"ext-mbstring": "*",
"ext-pdo": "*",
"alek13/slack": "^2.0",
"arietimmerman/laravel-scim-server": "dev-master",
"bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-debugbar": "^3.6",
"barryvdh/laravel-dompdf": "^1.0",
@ -61,7 +68,6 @@
"rollbar/rollbar-laravel": "^7.0",
"spatie/laravel-backup": "^6.16",
"tecnickcom/tc-lib-barcode": "^1.15",
"tightenco/ziggy": "v1.2.0",
"unicodeveloper/laravel-password": "^1.0",
"watson/validating": "^6.1"
},

330
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": "442a6af235589e35cfcaa7e5e39e75ec",
"content-hash": "ad10a039e46761e93b4461865b0e20f0",
"packages": [
{
"name": "alek13/slack",
@ -72,6 +72,69 @@
],
"time": "2021-10-20T22:52:32+00:00"
},
{
"name": "arietimmerman/laravel-scim-server",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/grokability/laravel-scim-server.git",
"reference": "10be959682d47bb8c78255168262a7cbb7586146"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/grokability/laravel-scim-server/zipball/10be959682d47bb8c78255168262a7cbb7586146",
"reference": "10be959682d47bb8c78255168262a7cbb7586146",
"shasum": ""
},
"require": {
"illuminate/console": "^6.0|^7.0|^8.0",
"illuminate/database": "^6.0|^7.0|^8.0",
"illuminate/support": "^6.0|^7.0|^8.0",
"php": "^7.0|^8.0",
"tmilos/scim-filter-parser": "^1.3",
"tmilos/scim-schema": "^0.1.0"
},
"require-dev": {
"laravel/legacy-factories": "*",
"orchestra/testbench": "^5.0|^6.0"
},
"default-branch": true,
"type": "library",
"extra": {
"laravel": {
"providers": [
"ArieTimmerman\\Laravel\\SCIMServer\\ServiceProvider"
],
"aliases": {
"SCIMServerHelper": "ArieTimmerman\\Laravel\\SCIMServer\\Helper"
}
}
},
"autoload": {
"psr-4": {
"ArieTimmerman\\Laravel\\SCIMServer\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"ArieTimmerman\\Laravel\\SCIMServer\\Tests\\": "tests"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Arie Timmerman",
"email": "arietimmerman@gmail.com"
}
],
"description": "Laravel Package for creating a SCIM server",
"support": {
"source": "https://github.com/grokability/laravel-scim-server/tree/master"
},
"time": "2022-03-31T19:29:59+00:00"
},
{
"name": "asm89/stack-cors",
"version": "v2.1.1",
@ -11030,72 +11093,6 @@
],
"time": "2021-12-31T09:40:23+00:00"
},
{
"name": "tightenco/ziggy",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/tighten/ziggy.git",
"reference": "147804d5f3e98b897fc1ed15efc2807f1099cf83"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tighten/ziggy/zipball/147804d5f3e98b897fc1ed15efc2807f1099cf83",
"reference": "147804d5f3e98b897fc1ed15efc2807f1099cf83",
"shasum": ""
},
"require": {
"laravel/framework": ">=5.4@dev"
},
"require-dev": {
"orchestra/testbench": "^6.0",
"phpunit/phpunit": "^9.2"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Tightenco\\Ziggy\\ZiggyServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Tightenco\\Ziggy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Coulbourne",
"email": "daniel@tighten.co"
},
{
"name": "Jake Bathman",
"email": "jake@tighten.co"
},
{
"name": "Jacob Baker-Kretzmar",
"email": "jacob@tighten.co"
}
],
"description": "Generates a Blade directive exporting all of your named Laravel routes. Also provides a nice route() helper function in JavaScript.",
"homepage": "https://github.com/tighten/ziggy",
"keywords": [
"Ziggy",
"javascript",
"laravel",
"routes"
],
"support": {
"issues": "https://github.com/tighten/ziggy/issues",
"source": "https://github.com/tighten/ziggy/tree/v1.2.0"
},
"time": "2021-05-24T22:46:59+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "2.2.4",
@ -11149,6 +11146,199 @@
},
"time": "2021-12-08T09:12:39+00:00"
},
{
"name": "tmilos/lexer",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/tmilos/lexer.git",
"reference": "e7885595614759f1da2ff79b66e3fb26d7f875fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tmilos/lexer/zipball/e7885595614759f1da2ff79b66e3fb26d7f875fa",
"reference": "e7885595614759f1da2ff79b66e3fb26d7f875fa",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "~5.6",
"satooshi/php-coveralls": "^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Tmilos\\Lexer\\": "src/",
"Tests\\Tmilos\\Lexer\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Milos Tomic",
"email": "tmilos@gmail.com"
}
],
"description": "Lexical analyzer with individual token definition with regular expressions",
"keywords": [
"lexer",
"parser"
],
"support": {
"issues": "https://github.com/tmilos/lexer/issues",
"source": "https://github.com/tmilos/lexer/tree/master"
},
"time": "2016-12-21T11:22:39+00:00"
},
{
"name": "tmilos/scim-filter-parser",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/tmilos/scim-filter-parser.git",
"reference": "cfd9ba1f33e1e15adcab2481bffd74cb9fb35704"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tmilos/scim-filter-parser/zipball/cfd9ba1f33e1e15adcab2481bffd74cb9fb35704",
"reference": "cfd9ba1f33e1e15adcab2481bffd74cb9fb35704",
"shasum": ""
},
"require": {
"tmilos/lexer": "^1.0",
"tmilos/value": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7",
"satooshi/php-coveralls": "^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Tmilos\\ScimFilterParser\\": "src/",
"Tests\\Tmilos\\ScimFilterParser\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Milos Tomic",
"email": "tmilos@gmail.com"
}
],
"description": "System for Cross-domain Identity Management SCIM AST filter parser PHP library",
"keywords": [
"SCIM AST",
"SCIM filter parser",
"SCIM parser",
"ast",
"parser",
"scim",
"simplecloud"
],
"support": {
"issues": "https://github.com/tmilos/scim-filter-parser/issues",
"source": "https://github.com/tmilos/scim-filter-parser/tree/master"
},
"time": "2017-01-19T11:17:42+00:00"
},
{
"name": "tmilos/scim-schema",
"version": "0.1",
"source": {
"type": "git",
"url": "https://github.com/tmilos/scim-schema.git",
"reference": "bb871e667b33080b4cd36d7a9b2ac2cdbf796062"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tmilos/scim-schema/zipball/bb871e667b33080b4cd36d7a9b2ac2cdbf796062",
"reference": "bb871e667b33080b4cd36d7a9b2ac2cdbf796062",
"shasum": ""
},
"require-dev": {
"phpunit/phpunit": "^4.8|^5.6",
"satooshi/php-coveralls": "^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Tmilos\\ScimSchema\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Milos Tomic",
"email": "tmilos@gmail.com"
}
],
"description": "SCIM schema library",
"support": {
"issues": "https://github.com/tmilos/scim-schema/issues",
"source": "https://github.com/tmilos/scim-schema/tree/master"
},
"time": "2017-11-25T22:18:16+00:00"
},
{
"name": "tmilos/value",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/tmilos/value.git",
"reference": "9e78ad9c026b14cacec1a27552ee0ada9d7d1c06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tmilos/value/zipball/9e78ad9c026b14cacec1a27552ee0ada9d7d1c06",
"reference": "9e78ad9c026b14cacec1a27552ee0ada9d7d1c06",
"shasum": ""
},
"require": {
"php": ">=5.5.1"
},
"require-dev": {
"moontoast/math": "~1.1",
"phpunit/phpunit": "~4.8",
"ramsey/uuid": "^3.3",
"satooshi/php-coveralls": "~0.6"
},
"type": "library",
"autoload": {
"psr-0": {
"Tmilos\\Value\\": "src/",
"Tmilos\\Value\\Tests\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Milos Tomic",
"email": "tmilos@gmail.com",
"homepage": "https://github.com/tmilos/",
"role": "Developer"
}
],
"support": {
"issues": "https://github.com/tmilos/value/issues",
"source": "https://github.com/tmilos/value/tree/master"
},
"time": "2016-06-06T10:22:16+00:00"
},
{
"name": "unicodeveloper/laravel-password",
"version": "1.0.4",
@ -13462,7 +13652,9 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"arietimmerman/laravel-scim-server": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@ -13474,5 +13666,5 @@
"ext-pdo": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.1.0"
}

View file

@ -342,7 +342,6 @@ return [
Laravel\Passport\PassportServiceProvider::class,
Laravel\Tinker\TinkerServiceProvider::class,
Unicodeveloper\DumbPassword\DumbPasswordServiceProvider::class,
Tightenco\Ziggy\ZiggyServiceProvider::class, // Laravel routes in vue
Eduardokum\LaravelMailAutoEmbed\ServiceProvider::class,
/*

5
config/scim.php Normal file
View file

@ -0,0 +1,5 @@
<?php
return [
"publish_routes" => false
];

View file

@ -1,738 +0,0 @@
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "/";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 2);
/******/ })
/************************************************************************/
/******/ ({
/***/ "./resources/assets/js/snipeit.js":
/***/ (function(module, exports) {
/**
* Module containing core application logic.
* @param {jQuery} $ Insulated jQuery object
* @param {JSON} settings Insulated `window.snipeit.settings` object.
* @return {IIFE} Immediately invoked. Returns self.
*/
lineOptions = {
legend: {
position: "bottom"
},
scales: {
yAxes: [{
ticks: {
fontColor: "rgba(0,0,0,0.5)",
fontStyle: "bold",
beginAtZero: true,
maxTicksLimit: 5,
padding: 20
},
gridLines: {
drawTicks: false,
display: false
}
}],
xAxes: [{
gridLines: {
zeroLineColor: "transparent"
},
ticks: {
padding: 20,
fontColor: "rgba(0,0,0,0.5)",
fontStyle: "bold"
}
}]
}
};
pieOptions = {
//Boolean - Whether we should show a stroke on each segment
segmentShowStroke: true,
//String - The colour of each segment stroke
segmentStrokeColor: "#fff",
//Number - The width of each segment stroke
segmentStrokeWidth: 1,
//Number - The percentage of the chart that we cut out of the middle
percentageInnerCutout: 50, // This is 0 for Pie charts
//Number - Amount of animation steps
animationSteps: 100,
//String - Animation easing effect
animationEasing: "easeOutBounce",
//Boolean - Whether we animate the rotation of the Doughnut
animateRotate: true,
//Boolean - Whether we animate scaling the Doughnut from the centre
animateScale: false,
//Boolean - whether to make the chart responsive to window resizing
responsive: true,
// Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
maintainAspectRatio: false,
//String - A legend template
legendTemplate: "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li>" + "<i class='fas fa-circle-o' style='color: <%=segments[i].fillColor%>'></i>" + "<%if(segments[i].label){%><%=segments[i].label%><%}%> foo</li><%}%></ul>",
//String - A tooltip template
tooltipTemplate: "<%=value %> <%=label%> "
};
//-----------------
//- END PIE CHART -
//-----------------
(function ($, settings) {
var Components = {};
Components.modals = {};
// confirm delete modal
Components.modals.confirmDelete = function () {
var $el = $('table');
var events = {
'click': function click(evnt) {
var $context = $(this);
var $dataConfirmModal = $('#dataConfirmModal');
var href = $context.attr('href');
var message = $context.attr('data-content');
var title = $context.attr('data-title');
$('#myModalLabel').text(title);
$dataConfirmModal.find('.modal-body').text(message);
$('#deleteForm').attr('action', href);
$dataConfirmModal.modal({
show: true
});
return false;
}
};
var render = function render() {
$el.on('click', '.delete-asset', events['click']);
};
return {
render: render
};
// confirm restore modal
Components.modals.confirmRestore = function () {
var $el = $('table');
var events = {
'click': function click(evnt) {
var $context = $(this);
var $dataConfirmModal = $('#restoreConfirmModal');
var href = $context.attr('href');
var message = $context.attr('data-content');
var title = $context.attr('data-title');
$('#myModalLabel').text(title);
$dataConfirmModal.find('.modal-body').text(message);
$('#confirmRestoreForm').attr('action', href);
$dataConfirmModal.modal({
show: true
});
return false;
}
};
var render = function render() {
$el.on('click', '.restore-modal', events['click']);
};
return {
render: render
};
};
/**
* Application start point
* Component definition stays out of load event, execution only happens.
*/
$(function () {
new Components.modals.confirmDelete().render();
});
})(jQuery, window.snipeit.settings);
$(document).ready(function () {
/*
* Slideout help menu
*/
$('.slideout-menu-toggle').on('click', function (event) {
console.log('clicked');
event.preventDefault();
// create menu variables
var slideoutMenu = $('.slideout-menu');
var slideoutMenuWidth = $('.slideout-menu').width();
// toggle open class
slideoutMenu.toggleClass("open");
// slide menu
if (slideoutMenu.hasClass("open")) {
slideoutMenu.show();
slideoutMenu.animate({
right: "0px"
});
} else {
slideoutMenu.animate({
right: -slideoutMenuWidth
}, "-350px");
slideoutMenu.fadeOut();
}
});
/*
* iCheck checkbox plugin
*/
$('input[type="checkbox"].minimal, input[type="radio"].minimal').iCheck({
checkboxClass: 'icheckbox_minimal-blue',
radioClass: 'iradio_minimal-blue'
});
/*
* Select2
*/
var iOS = /iPhone|iPad|iPod/.test(navigator.userAgent) && !window.MSStream;
if (!iOS) {
// Vue collision: Avoid overriding a vue select2 instance
// by checking to see if the item has already been select2'd.
$('select.select2:not(".select2-hidden-accessible")').each(function (i, obj) {
{
$(obj).select2();
}
});
}
$('.datepicker').datepicker();
// var datepicker = $.fn.datepicker.noConflict(); // return $.fn.datepicker to previously assigned value
// $.fn.bootstrapDP = datepicker;
// $('.datepicker').datepicker();
// Crazy select2 rich dropdowns with images!
$('.js-data-ajax').each(function (i, item) {
var link = $(item);
var endpoint = link.data("endpoint");
var select = link.data("select");
link.select2({
/**
* Adds an empty placeholder, allowing every select2 instance to be cleared.
* This placeholder can be overridden with the "data-placeholder" attribute.
*/
placeholder: '',
allowClear: true,
ajax: {
// the baseUrl includes a trailing slash
url: Ziggy.baseUrl + 'api/v1/' + endpoint + '/selectlist',
dataType: 'json',
delay: 250,
headers: {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
},
data: function data(params) {
var data = {
search: params.term,
page: params.page || 1,
assetStatusType: link.data("asset-status-type")
};
return data;
},
processResults: function processResults(data, params) {
params.page = params.page || 1;
var answer = {
results: data.items,
pagination: {
more: "true" //(params.page < data.page_count)
}
};
return answer;
},
cache: true
},
escapeMarkup: function escapeMarkup(markup) {
return markup;
}, // let our custom formatter work
templateResult: formatDatalist,
templateSelection: formatDataSelection
});
});
function getSelect2Value(element) {
// if the passed object is not a jquery object, assuming 'element' is a selector
if (!(element instanceof jQuery)) element = $(element);
var select = element.data("select2");
// There's two different locations where the select2-generated input element can be.
searchElement = select.dropdown.$search || select.$container.find(".select2-search__field");
var value = searchElement.val();
return value;
}
$(".select2-hidden-accessible").on('select2:selecting', function (e) {
var data = e.params.args.data;
var isMouseUp = false;
var element = $(this);
var value = getSelect2Value(element);
if (e.params.args.originalEvent) isMouseUp = e.params.args.originalEvent.type == "mouseup";
// if selected item does not match typed text, do not allow it to pass - force close for ajax.
if (!isMouseUp) {
if (value.toLowerCase() && data.text.toLowerCase().indexOf(value) < 0) {
e.preventDefault();
element.select2('close');
// if it does match, we set a flag in the event (which gets passed to subsequent events), telling it not to worry about the ajax
} else if (value.toLowerCase() && data.text.toLowerCase().indexOf(value) > -1) {
e.params.args.noForceAjax = true;
}
}
});
$(".select2-hidden-accessible").on('select2:closing', function (e) {
var element = $(this);
var value = getSelect2Value(element);
var noForceAjax = false;
var isMouseUp = false;
if (e.params.args.originalSelect2Event) noForceAjax = e.params.args.originalSelect2Event.noForceAjax;
if (e.params.args.originalEvent) isMouseUp = e.params.args.originalEvent.type == "mouseup";
if (value && !noForceAjax && !isMouseUp) {
var endpoint = element.data("endpoint");
var assetStatusType = element.data("asset-status-type");
$.ajax({
url: Ziggy.baseUrl + 'api/v1/' + endpoint + '/selectlist?search=' + value + '&page=1' + (assetStatusType ? '&assetStatusType=' + assetStatusType : ''),
dataType: 'json',
headers: {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
}
}).done(function (response) {
;
var currentlySelected = element.select2('data').map(function (x) {
return +x.id;
}).filter(function (x) {
return x !== 0;
});
// makes sure we're not selecting the same thing twice for multiples
var filteredResponse = response.items.filter(function (item) {
return currentlySelected.indexOf(+item.id) < 0;
});
var first = currentlySelected.length > 0 ? filteredResponse[0] : response.items[0];
if (first && first.id) {
first.selected = true;
if ($("option[value='" + first.id + "']", element).length < 1) {
var option = new Option(first.text, first.id, true, true);
element.append(option);
} else {
var isMultiple = element.attr("multiple") == "multiple";
element.val(isMultiple ? element.val().concat(first.id) : element.val(first.id));
}
element.trigger('change');
element.trigger({
type: 'select2:select',
params: {
data: first
}
});
}
});
}
});
function formatDatalist(datalist) {
var loading_markup = '<i class="fas fa-spinner fa-spin" aria-hidden="true"></i> Loading...';
if (datalist.loading) {
return loading_markup;
}
var markup = "<div class='clearfix'>";
markup += "<div class='pull-left' style='padding-right: 10px;'>";
if (datalist.image) {
markup += "<div style='width: 30px;'><img src='" + datalist.image + "' style='max-height: 20px; max-width: 30px;'></div>";
} else {
markup += "<div style='height: 20px; width: 30px;'></div>";
}
markup += "</div><div>" + datalist.text + "</div>";
markup += "</div>";
return markup;
}
function formatDataSelection(datalist) {
return datalist.text;
}
// This handles the radio button selectors for the checkout-to-foo options
// on asset checkout and also on asset edit
$(function () {
$('input[name=checkout_to_type]').on("change", function () {
var assignto_type = $('input[name=checkout_to_type]:checked').val();
var userid = $('#assigned_user option:selected').val();
if (assignto_type == 'asset') {
$('#current_assets_box').fadeOut();
$('#assigned_asset').show();
$('#assigned_user').hide();
$('#assigned_location').hide();
$('.notification-callout').fadeOut();
} else if (assignto_type == 'location') {
$('#current_assets_box').fadeOut();
$('#assigned_asset').hide();
$('#assigned_user').hide();
$('#assigned_location').show();
$('.notification-callout').fadeOut();
} else {
$('#assigned_asset').hide();
$('#assigned_user').show();
$('#assigned_location').hide();
if (userid) {
$('#current_assets_box').fadeIn();
}
$('.notification-callout').fadeIn();
}
});
});
// ------------------------------------------------
// Deep linking for Bootstrap tabs
// ------------------------------------------------
var taburl = document.location.toString();
// Allow full page URL to activate a tab's ID
// ------------------------------------------------
// This allows linking to a tab on page load via the address bar.
// So a URL such as, http://snipe-it.local/hardware/2/#my_tab will
// cause the tab on that page with an ID of “my_tab” to be active.
if (taburl.match('#')) {
$('.nav-tabs a[href="#' + taburl.split('#')[1] + '"]').tab('show');
}
// Allow internal page links to activate a tab's ID.
// ------------------------------------------------
// This allows you to link to a tab from anywhere on the page
// including from within another tab. Also note that internal page
// links either inside or out of the tabs need to include data-toggle="tab"
// Ex: <a href="#my_tab" data-toggle="tab">Click me</a>
$('a[data-toggle="tab"]').click(function (e) {
var href = $(this).attr("href");
history.pushState(null, null, href);
e.preventDefault();
$('a[href="' + $(this).attr('href') + '"]').tab('show');
});
// ------------------------------------------------
// End Deep Linking for Bootstrap tabs
// ------------------------------------------------
// Image preview
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
$('#imagePreview').attr('src', e.target.result);
};
reader.readAsDataURL(input.files[0]);
}
}
function formatBytes(bytes) {
if (bytes < 1024) return bytes + " Bytes";else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + " KB";else if (bytes < 1073741824) return (bytes / 1048576).toFixed(2) + " MB";else return (bytes / 1073741824).toFixed(2) + " GB";
};
// File size validation
$('#uploadFile').bind('change', function () {
$('#upload-file-status').removeClass('text-success').removeClass('text-danger');
$('.goodfile').remove();
$('.badfile').remove();
$('.badfile').remove();
$('.previewSize').hide();
$('#upload-file-info').html('');
var max_size = $('#uploadFile').data('maxsize');
var total_size = 0;
for (var i = 0; i < this.files.length; i++) {
total_size += this.files[i].size;
$('#upload-file-info').append('<span class="label label-default">' + this.files[i].name + ' (' + formatBytes(this.files[i].size) + ')</span> ');
}
if (total_size > max_size) {
$('#upload-file-status').addClass('text-danger').removeClass('help-block').prepend('<i class="badfile fas fa-times"></i> ').append('<span class="previewSize"> Upload is ' + formatBytes(total_size) + '.</span>');
} else {
$('#upload-file-status').addClass('text-success').removeClass('help-block').prepend('<i class="goodfile fas fa-check"></i> ');
readURL(this);
$('#imagePreview').fadeIn();
}
});
});
/***/ }),
/***/ "./resources/assets/js/snipeit_modals.js":
/***/ (function(module, exports) {
/*
*
* Snipe-IT Universal Modal support
*
* Enables modal dialogs to create sub-resources throughout Snipe-IT
*
*/
/*
HOW TO USE
Create a Button looking like this:
<a href='{{ route('modal.user') }}' data-toggle="modal" data-target="#createModal" data-select='assigned_to' class="btn btn-sm btn-default">New</a>
If you don't have access to Blade commands (like {{ and }}, etc), you can hard-code a URL as the 'href'
data-toggle="modal" - required for Bootstrap Modals
data-target="#createModal" - fixed ID for the modal, do not change
data-select="assigned_to" - What is the *ID* of the select-dropdown that you're going to be adding to, if the modal-create was a success? Be on the lookout for duplicate ID's, it will confuse this library!
class="btn btn-sm btn-default" - makes it look button-ey, feel free to change :)
If you want to pass additional variables to the modal (In the Category Create one, for example, you can pass category_id), you can encode them as URL variables in the href
*/
$(function () {
//handle modal-add-interstitial calls
var model, select;
if ($('#createModal').length == 0) {
$('body').append('<div class="modal fade" id="createModal"></div><!-- /.modal -->');
}
$('#createModal').on("show.bs.modal", function (event) {
var link = $(event.relatedTarget);
model = link.data("dependency");
select = link.data("select");
$('#createModal').load(link.attr('href'), function () {
//do we need to re-select2 this, after load? Probably.
$('#createModal').find('select.select2').select2();
// Initialize the ajaxy select2 with images.
// This is a copy/paste of the code from snipeit.js, would be great to only have this in one place.
$('.js-data-ajax').each(function (i, item) {
var link = $(item);
var endpoint = link.data("endpoint");
var select = link.data("select");
link.select2({
ajax: {
// the baseUrl includes a trailing slash
url: Ziggy.baseUrl + 'api/v1/' + endpoint + '/selectlist',
dataType: 'json',
delay: 250,
headers: {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
},
data: function data(params) {
var data = {
search: params.term,
page: params.page || 1,
assetStatusType: link.data("asset-status-type")
};
return data;
},
processResults: function processResults(data, params) {
params.page = params.page || 1;
var answer = {
results: data.items,
pagination: {
more: "true" //(params.page < data.page_count)
}
};
return answer;
},
cache: true
},
escapeMarkup: function escapeMarkup(markup) {
return markup;
}, // let our custom formatter work
templateResult: formatDatalist,
templateSelection: formatDataSelection
});
});
});
});
$('#createModal').on('click', '#modal-save', function () {
$.ajax({
type: 'POST',
url: $('.modal-body form').attr('action'),
headers: {
"X-Requested-With": 'XMLHttpRequest',
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr('content')
},
data: $('.modal-body form').serialize(),
success: function success(result) {
if (result.status == "error") {
var error_message = "";
for (var field in result.messages) {
error_message += "<li>Problem(s) with field <i><strong>" + field + "</strong></i>: " + result.messages[field];
}
$('#modal_error_msg').html(error_message).show();
return false;
}
var id = result.payload.id;
var name = result.payload.name || result.payload.first_name + " " + result.payload.last_name;
if (!id || !name) {
console.error("Could not find resulting name or ID from modal-create. Name: " + name + ", id: " + id);
return false;
}
$('#createModal').modal('hide');
$('#createModal').html("");
// "select" is the original drop-down menu that someone
// clicked 'add' on to add a new 'thing'
// this code adds the newly created object to that select
var selector = document.getElementById(select);
if (!selector) {
return false;
}
selector.options[selector.length] = new Option(name, id);
selector.selectedIndex = selector.length - 1;
$(selector).trigger("change");
if (window.fetchCustomFields) {
fetchCustomFields();
}
},
error: function error(result) {
msg = result.responseJSON.messages || result.responseJSON.error;
$('#modal_error_msg').html("Server Error: " + msg).show();
}
});
});
});
function formatDatalist(datalist) {
var loading_markup = '<i class="fas fa-spinner fa-spin" aria-hidden="true"></i> Loading...';
if (datalist.loading) {
return loading_markup;
}
var markup = "<div class='clearfix'>";
markup += "<div class='pull-left' style='padding-right: 10px;'>";
if (datalist.image) {
markup += "<div style='width: 30px;'><img src='" + datalist.image + "' style='max-height: 20px; max-width: 30px;'></div>";
} else {
markup += "<div style='height: 20px; width: 30px;'></div>";
}
markup += "</div><div>" + datalist.text + "</div>";
markup += "</div>";
return markup;
}
function formatDataSelection(datalist) {
return datalist.text;
}
/***/ }),
/***/ 2:
/***/ (function(module, exports, __webpack_require__) {
__webpack_require__("./resources/assets/js/snipeit.js");
module.exports = __webpack_require__("./resources/assets/js/snipeit_modals.js");
/***/ })
/******/ });

View file

@ -100,6 +100,7 @@
</template>
<script>
var baseUrl = $('meta[name="baseUrl"]').attr('content');
export default {
props: ['file', 'customFields'],
data() {
@ -266,7 +267,7 @@
}
this.statusType='pending';
this.statusText = "Processing...";
this.$http.post(route('api.imports.importFile', this.file.id), {
this.$http.post(baseUrl + 'api/v1/imports/process/' + this.file.id, {
'import-update': this.options.update,
'send-welcome': this.options.send_welcome,
'import-type': this.options.importType,

View file

@ -2,6 +2,7 @@
<script>
require('blueimp-file-upload');
var baseUrl = $('meta[name="baseUrl"]').attr('content');
export default {
/*
* The component's data.
@ -63,7 +64,7 @@
methods: {
fetchFiles() {
this.$http.get(route('api.imports.index'))
this.$http.get(baseUrl + 'api/v1/imports')
.then( ({data}) => this.files = data, // Success
//Fail
(response) => {
@ -73,7 +74,7 @@
});
},
fetchCustomFields() {
this.$http.get(route('api.customfields.index'))
this.$http.get(baseUrl + 'api/v1/fields')
.then( ({data}) => {
data = data.rows;
data.forEach((item) => {
@ -85,7 +86,7 @@
});
},
deleteFile(file, key) {
this.$http.delete(route('api.imports.destroy', file.id))
this.$http.delete(baseUrl + 'api/v1/imports/' + file.id)
.then(
// Success, remove file from array.
(response) => {

View file

@ -73,8 +73,6 @@
}
};
</script>
<!-- Add laravel routes into javascript Primarily useful for vue.-->
@routes
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<script src="{{ url(asset('js/html5shiv.js')) }}" nonce="{{ csrf_token() }}"></script>

38
routes/scim.php Normal file
View file

@ -0,0 +1,38 @@
<?php
use ArieTimmerman\Laravel\SCIMServer\RouteProvider as SCIMRouteProvider;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| SCIM Routes
|--------------------------------------------------------------------------
|
| These are the routes that we have to explicitly inject from the
| laravel-scim-server project, which gives Snipe-IT SCIM support
|
*/
SCIMRouteProvider::publicRoutes(); // Make sure to load public routes *FIRST*
Route::middleware(['auth:api','authorize:superadmin'])->group(function () {
SCIMRouteProvider::routes(
[
/*
* If we leave public_routes as 'true', the public routes will load *now* and
* be jammed into the same middleware that these private routes are loaded
* with. That's bad, because these routes are *supposed* to be public.
*
* We loaded them a few lines above, *first*, otherwise the various
* fallback routes in the library defined within these *secured* routes
* will "take over" the above routes - and then you will end up losing
* like 4 hours of your life trying to figure out why the public routes
* aren't quite working right. Ask me how I know (BMW, 3/19/2022)
*/
'public_routes' => false
]
);
SCIMRouteProvider::meRoutes();
}); // ->can('superuser');