From 6756dd193eb9dd5deb9f622bbeb3fb523a886836 Mon Sep 17 00:00:00 2001 From: Brady Wetherington Date: Fri, 4 Mar 2022 14:00:00 -0800 Subject: [PATCH] SCIM integration using the 're-do-the routes' approach, which seems like a dead-end Cleaning up routes to match laravel-scim-server's recommended implementation Some actually *working* changes for SCIM support?! Whoops, forgot my route file Fix public SCIM routes Removed Ziggy, removed old generated file, yanked Ziggy references Resolves the first set of comments for SCIM Ensure all /api routes have baseUrl prepended Fix the parent:: call to be, uh, actually correct :P Clarify the route-ordering, as it is quite tricky This gets it so that users can actually be saved.. Work around the lack of callbacks with some inheritance Mapped a bunch more fields from SCIM into Snipe-IT's user table More baseUrl shenanigans since we yanked Ziggy :/ Properly map job title and work with some other necessary attributes Map more fields... Finalized basic mapping for core and enterprise namespaces Latest tuned settings for SCIM config to work with Azure (and others) --- app/Models/SCIMUser.php | 16 + app/Models/SnipeSCIMConfig.php | 174 +++++ app/Providers/AppServiceProvider.php | 3 + app/Providers/RouteServiceProvider.php | 2 +- composer.json | 2 +- composer.lock | 322 ++++++-- config/app.php | 1 - config/scim.php | 5 + public/js/snipeit.js | 738 ------------------ .../js/components/importer/importer-file.vue | 3 +- .../js/components/importer/importer.vue | 7 +- resources/views/layouts/default.blade.php | 2 - routes/scim.php | 38 + 13 files changed, 498 insertions(+), 815 deletions(-) create mode 100644 app/Models/SCIMUser.php create mode 100644 app/Models/SnipeSCIMConfig.php create mode 100644 config/scim.php delete mode 100644 public/js/snipeit.js create mode 100644 routes/scim.php diff --git a/app/Models/SCIMUser.php b/app/Models/SCIMUser.php new file mode 100644 index 000000000..71bd9169a --- /dev/null +++ b/app/Models/SCIMUser.php @@ -0,0 +1,16 @@ +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; + } + +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index c4110a589..29e581709 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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 } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 16a22d866..447f1bc30 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -24,7 +24,7 @@ class RouteServiceProvider extends ServiceProvider $this->mapWebRoutes(); - // + require base_path('routes/scim.php'); }); } diff --git a/composer.json b/composer.json index d3261eadf..712164f74 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "ext-mbstring": "*", "ext-pdo": "*", "alek13/slack": "^2.0", + "arietimmerman/laravel-scim-server": "^0.5.5", "bacon/bacon-qr-code": "^2.0", "barryvdh/laravel-debugbar": "^3.6", "barryvdh/laravel-dompdf": "^1.0", @@ -61,7 +62,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" }, diff --git a/composer.lock b/composer.lock index 68671abb8..42b5f2e9a 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "448c7508ab99eb86eb62e5cac3e9ee59", "packages": [ { "name": "alek13/slack", @@ -72,6 +72,65 @@ ], "time": "2021-10-20T22:52:32+00:00" }, + { + "name": "arietimmerman/laravel-scim-server", + "version": "v0.5.5", + "source": { + "type": "git", + "url": "https://github.com/arietimmerman/laravel-scim-server.git", + "reference": "8fb5b1cc0d28ace820b5b38a543d801fd49ada90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/arietimmerman/laravel-scim-server/zipball/8fb5b1cc0d28ace820b5b38a543d801fd49ada90", + "reference": "8fb5b1cc0d28ace820b5b38a543d801fd49ada90", + "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" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "ArieTimmerman\\Laravel\\SCIMServer\\ServiceProvider" + ], + "aliases": { + "SCIMServerHelper": "ArieTimmerman\\Laravel\\SCIMServer\\Helper" + } + } + }, + "autoload": { + "psr-4": { + "ArieTimmerman\\Laravel\\SCIMServer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arie Timmerman", + "email": "arietimmerman@gmail.com" + } + ], + "description": "Laravel Package for creating a SCIM server", + "support": { + "issues": "https://github.com/arietimmerman/laravel-scim-server/issues", + "source": "https://github.com/arietimmerman/laravel-scim-server/tree/v0.5.5" + }, + "time": "2022-02-03T20:56:31+00:00" + }, { "name": "asm89/stack-cors", "version": "v2.1.1", @@ -11030,72 +11089,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 +11142,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", @@ -13474,5 +13660,5 @@ "ext-pdo": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.1.0" } diff --git a/config/app.php b/config/app.php index d9fc45bb0..ba56b42e3 100755 --- a/config/app.php +++ b/config/app.php @@ -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, /* diff --git a/config/scim.php b/config/scim.php new file mode 100644 index 000000000..3008e2f07 --- /dev/null +++ b/config/scim.php @@ -0,0 +1,5 @@ + false +]; diff --git a/public/js/snipeit.js b/public/js/snipeit.js deleted file mode 100644 index 81c79c884..000000000 --- a/public/js/snipeit.js +++ /dev/null @@ -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: "", - //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 = ' Loading...'; - if (datalist.loading) { - return loading_markup; - } - - var markup = "
"; - markup += "
"; - if (datalist.image) { - markup += "
"; - } else { - markup += "
"; - } - - markup += "
" + datalist.text + "
"; - markup += "
"; - 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: Click me - $('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('' + this.files[i].name + ' (' + formatBytes(this.files[i].size) + ') '); - } - - if (total_size > max_size) { - $('#upload-file-status').addClass('text-danger').removeClass('help-block').prepend(' ').append(' Upload is ' + formatBytes(total_size) + '.'); - } else { - $('#upload-file-status').addClass('text-success').removeClass('help-block').prepend(' '); - 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: - New - 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(''); - } - - $('#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 += "
  • Problem(s) with field " + field + ": " + 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 = ' Loading...'; - if (datalist.loading) { - return loading_markup; - } - - var markup = "
    "; - markup += "
    "; - if (datalist.image) { - markup += "
    "; - } else { - markup += "
    "; - } - - markup += "
    " + datalist.text + "
    "; - markup += "
    "; - 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"); - - -/***/ }) - -/******/ }); \ No newline at end of file diff --git a/resources/assets/js/components/importer/importer-file.vue b/resources/assets/js/components/importer/importer-file.vue index 2f692db01..356d75c5c 100644 --- a/resources/assets/js/components/importer/importer-file.vue +++ b/resources/assets/js/components/importer/importer-file.vue @@ -100,6 +100,7 @@ - - @routes diff --git a/routes/scim.php b/routes/scim.php new file mode 100644 index 000000000..d8d8a8fdb --- /dev/null +++ b/routes/scim.php @@ -0,0 +1,38 @@ +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');