Merge branch 'develop' into features/lock_logins_to_saml

This commit is contained in:
Alex Janes 2022-01-11 09:05:14 -05:00 committed by GitHub
commit edef640d35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
180 changed files with 26265 additions and 23225 deletions

View file

@ -2531,6 +2531,24 @@
"contributions": [
"code"
]
},
{
"login": "adagioajanes",
"name": "Alex Janes",
"avatar_url": "https://avatars.githubusercontent.com/u/38761237?v=4",
"profile": "https://adagiohealth.org",
"contributions": [
"code"
]
},
{
"login": "nuraeil",
"name": "Nuraeil",
"avatar_url": "https://avatars.githubusercontent.com/u/32387849?v=4",
"profile": "https://github.com/nuraeil",
"contributions": [
"code"
]
}
]
}

View file

@ -1,5 +1,5 @@
![Build Status](https://app.chipperci.com/projects/0e5f8979-31eb-4ee6-9abf-050b76ab0383/status/master) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/snipe-it/localized.svg)](https://crowdin.com/project/snipe-it) [![Docker Pulls](https://img.shields.io/docker/pulls/snipe/snipe-it.svg)](https://hub.docker.com/r/snipe/snipe-it/) [![Twitter Follow](https://img.shields.io/twitter/follow/snipeitapp.svg?style=social)](https://twitter.com/snipeitapp) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/553ce52037fc43ea99149785afcfe641)](https://www.codacy.com/app/snipe/snipe-it?utm_source=github.com&utm_medium=referral&utm_content=snipe/snipe-it&utm_campaign=Badge_Grade)
[![All Contributors](https://img.shields.io/badge/all_contributors-277-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev)
[![All Contributors](https://img.shields.io/badge/all_contributors-280-orange.svg?style=flat-square)](#contributors) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/yZFtShAcKk) [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev)
## Snipe-IT - Open Source Asset Management System
@ -128,11 +128,9 @@ Thanks goes to all of these wonderful people ([emoji key](https://github.com/ken
| [<img src="https://avatars.githubusercontent.com/u/9255772?v=4" width="110px;"/><br /><sub>Mark Stenglein</sub>](https://markstenglein.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ocelotsloth "Code") | [<img src="https://avatars.githubusercontent.com/u/35658596?v=4" width="110px;"/><br /><sub>ajsy</sub>](https://github.com/ajsy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=ajsy "Code") | [<img src="https://avatars.githubusercontent.com/u/3628035?v=4" width="110px;"/><br /><sub>Jan Kiesewetter</sub>](https://github.com/t3easy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=t3easy "Code") | [<img src="https://avatars.githubusercontent.com/u/79449630?v=4" width="110px;"/><br /><sub>Tetrachloromethane250</sub>](https://github.com/Tetrachloromethane250)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Tetrachloromethane250 "Code") | [<img src="https://avatars.githubusercontent.com/u/22004482?v=4" width="110px;"/><br /><sub>Lars Kajes</sub>](https://www.kajes.se/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kajes "Code") | [<img src="https://avatars.githubusercontent.com/u/13993216?v=4" width="110px;"/><br /><sub>Joly0</sub>](https://github.com/Joly0)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Joly0 "Code") | [<img src="https://avatars.githubusercontent.com/u/1501022?v=4" width="110px;"/><br /><sub>theburger</sub>](https://github.com/limeless)<br />[💻](https://github.com/snipe/snipe-it/commits?author=limeless "Code") |
| [<img src="https://avatars.githubusercontent.com/u/36065681?v=4" width="110px;"/><br /><sub>David Valin Alonso</sub>](https://github.com/deivishome)<br />[💻](https://github.com/snipe/snipe-it/commits?author=deivishome "Code") | [<img src="https://avatars.githubusercontent.com/u/8290389?v=4" width="110px;"/><br /><sub>andreaci</sub>](https://github.com/andreaci)<br />[💻](https://github.com/snipe/snipe-it/commits?author=andreaci "Code") | [<img src="https://avatars.githubusercontent.com/u/1828542?v=4" width="110px;"/><br /><sub>Jelle Sebreghts</sub>](http://www.jellesebreghts.be)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Jelle-S "Code") | [<img src="https://avatars.githubusercontent.com/u/11180862?v=4" width="110px;"/><br /><sub>Michael Pietsch</sub>](https://github.com/Skywalker-11)<br /> | [<img src="https://avatars.githubusercontent.com/u/22068886?v=4" width="110px;"/><br /><sub>Masudul Haque Shihab</sub>](https://github.com/sh1hab)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sh1hab "Code") | [<img src="https://avatars.githubusercontent.com/u/16099942?v=4" width="110px;"/><br /><sub>Supapong Areeprasertkul</sub>](http://www.freedomdive.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=zybersup "Code") | [<img src="https://avatars.githubusercontent.com/u/207358?v=4" width="110px;"/><br /><sub>Peter Sarossy</sub>](https://github.com/psarossy)<br />[💻](https://github.com/snipe/snipe-it/commits?author=psarossy "Code") |
| [<img src="https://avatars.githubusercontent.com/u/11823649?v=4" width="110px;"/><br /><sub>Renee Margaret McConahy</sub>](https://github.com/nepella)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nepella "Code") | [<img src="https://avatars.githubusercontent.com/u/5553884?v=4" width="110px;"/><br /><sub>JohnnyPicnic</sub>](https://github.com/JohnnyPicnic)<br />[💻](https://github.com/snipe/snipe-it/commits?author=JohnnyPicnic "Code") | [<img src="https://avatars.githubusercontent.com/u/8799594?v=4" width="110px;"/><br /><sub>markbrule</sub>](https://github.com/markbrule)<br />[💻](https://github.com/snipe/snipe-it/commits?author=markbrule "Code") | [<img src="https://avatars.githubusercontent.com/u/1962801?v=4" width="110px;"/><br /><sub>Mike Campbell</sub>](https://github.com/mikecmpbll)<br />[💻](https://github.com/snipe/snipe-it/commits?author=mikecmpbll "Code") | [<img src="https://avatars.githubusercontent.com/u/11973217?v=4" width="110px;"/><br /><sub>tbrconnect</sub>](https://github.com/tbrconnect)<br />[💻](https://github.com/snipe/snipe-it/commits?author=tbrconnect "Code") | [<img src="https://avatars.githubusercontent.com/u/12447225?v=4" width="110px;"/><br /><sub>kcoyo</sub>](https://github.com/kcoyo)<br />[💻](https://github.com/snipe/snipe-it/commits?author=kcoyo "Code") | [<img src="https://avatars.githubusercontent.com/u/494017?v=4" width="110px;"/><br /><sub>Travis Miller</sub>](https://travismiller.com/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=travismiller "Code") |
| [<img src="https://avatars.githubusercontent.com/u/8735148?v=4" width="110px;"/><br /><sub>Petri Asikainen</sub>](https://github.com/PetriAsi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PetriAsi "Code") | [<img src="https://avatars.githubusercontent.com/u/11424540?v=4" width="110px;"/><br /><sub>derdeagle</sub>](https://github.com/derdeagle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derdeagle "Code") | [<img src="https://avatars.githubusercontent.com/u/176950?v=4" width="110px;"/><br /><sub>Mike Frysinger</sub>](https://wh0rd.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vapier "Code") | [<img src="https://avatars.githubusercontent.com/u/22044358?v=4" width="110px;"/><br /><sub>ALPHA</sub>](https://github.com/AL4AL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AL4AL "Code") | [<img src="https://avatars.githubusercontent.com/u/1042587?v=4" width="110px;"/><br /><sub>FliegenKLATSCH</sub>](https://www.ifern.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH "Code") | [<img src="https://avatars.githubusercontent.com/u/442138?v=4" width="110px;"/><br /><sub>Jeremy Price</sub>](https://github.com/jerm)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jerm "Code") | [<img src="https://avatars.githubusercontent.com/u/84392209?v=4" width="110px;"/><br /><sub>Toreg87</sub>](https://github.com/Toreg87)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Toreg87 "Code") |
| [<img src="https://avatars.githubusercontent.com/u/67638596?v=4" width="110px;"/><br /><sub>Matthew Nickson</sub>](https://github.com/Computroniks)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Computroniks "Code") | [<img src="https://avatars.githubusercontent.com/u/1646397?v=4" width="110px;"/><br /><sub>Jethro Nederhof</sub>](https://jethron.id.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jethron "Code") | [<img src="https://avatars.githubusercontent.com/u/23289826?v=4" width="110px;"/><br /><sub>Oskar Stenberg</sub>](https://github.com/01ste02)<br />[💻](https://github.com/snipe/snipe-it/commits?author=01ste02 "Code") | [<img src="https://avatars.githubusercontent.com/u/82208283?v=4" width="110px;"/><br /><sub>Robert-Azelis</sub>](https://github.com/Robert-Azelis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Robert-Azelis "Code") | [<img src="https://avatars.githubusercontent.com/u/60648387?v=4" width="110px;"/><br /><sub>Alexander William Smith</sub>](https://github.com/alwism)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alwism "Code") | [<img src="https://avatars.githubusercontent.com/u/24418301?v=4" width="110px;"/><br /><sub>LEITWERK AG</sub>](https://www.leitwerk.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leitwerk-ag "Code") | [<img src="https://avatars.githubusercontent.com/u/1911435?v=4" width="110px;"/><br /><sub>Adam</sub>](http://www.aboutcher.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamboutcher "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1975640?v=4" width="110px;"/><br /><sub>Evan Taylor</sub>](https://github.com/Delta5)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Delta5 "Code") | [<img src="https://avatars.githubusercontent.com/u/8735148?v=4" width="110px;"/><br /><sub>Petri Asikainen</sub>](https://github.com/PetriAsi)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PetriAsi "Code") | [<img src="https://avatars.githubusercontent.com/u/11424540?v=4" width="110px;"/><br /><sub>derdeagle</sub>](https://github.com/derdeagle)<br />[💻](https://github.com/snipe/snipe-it/commits?author=derdeagle "Code") | [<img src="https://avatars.githubusercontent.com/u/176950?v=4" width="110px;"/><br /><sub>Mike Frysinger</sub>](https://wh0rd.org/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=vapier "Code") | [<img src="https://avatars.githubusercontent.com/u/22044358?v=4" width="110px;"/><br /><sub>ALPHA</sub>](https://github.com/AL4AL)<br />[💻](https://github.com/snipe/snipe-it/commits?author=AL4AL "Code") | [<img src="https://avatars.githubusercontent.com/u/1042587?v=4" width="110px;"/><br /><sub>FliegenKLATSCH</sub>](https://www.ifern.de)<br />[💻](https://github.com/snipe/snipe-it/commits?author=FliegenKLATSCH "Code") | [<img src="https://avatars.githubusercontent.com/u/442138?v=4" width="110px;"/><br /><sub>Jeremy Price</sub>](https://github.com/jerm)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jerm "Code") |
| [<img src="https://avatars.githubusercontent.com/u/84392209?v=4" width="110px;"/><br /><sub>Toreg87</sub>](https://github.com/Toreg87)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Toreg87 "Code") | [<img src="https://avatars.githubusercontent.com/u/67638596?v=4" width="110px;"/><br /><sub>Matthew Nickson</sub>](https://github.com/Computroniks)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Computroniks "Code") | [<img src="https://avatars.githubusercontent.com/u/1646397?v=4" width="110px;"/><br /><sub>Jethro Nederhof</sub>](https://jethron.id.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jethron "Code") | [<img src="https://avatars.githubusercontent.com/u/23289826?v=4" width="110px;"/><br /><sub>Oskar Stenberg</sub>](https://github.com/01ste02)<br />[💻](https://github.com/snipe/snipe-it/commits?author=01ste02 "Code") | [<img src="https://avatars.githubusercontent.com/u/82208283?v=4" width="110px;"/><br /><sub>Robert-Azelis</sub>](https://github.com/Robert-Azelis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Robert-Azelis "Code") | [<img src="https://avatars.githubusercontent.com/u/60648387?v=4" width="110px;"/><br /><sub>Alexander William Smith</sub>](https://github.com/alwism)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alwism "Code") | [<img src="https://avatars.githubusercontent.com/u/88776392?v=4" width="110px;"/><br /><sub>PlaneNuts</sub>](https://github.com/PlaneNuts)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PlaneNuts "Code") |
| [<img src="https://avatars.githubusercontent.com/u/16104273?v=4" width="110px;"/><br /><sub>Ian</sub>](https://snksrv.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sneak-it "Code") | [<img src="https://avatars.githubusercontent.com/u/4023909?v=4" width="110px;"/><br /><sub>Shao Yu-Lung (Allen)</sub>](http://blog.bestlong.idv.tw/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bestlong "Code") | [<img src="https://avatars.githubusercontent.com/u/76475453?v=4" width="110px;"/><br /><sub>Haxatron</sub>](https://github.com/Haxatron)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Haxatron "Code") | [<img src="https://avatars.githubusercontent.com/u/3842948?v=4" width="110px;"/><br /><sub>Bradley Coudriet</sub>](http://bjcpgd.cias.rit.edu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=exula "Code") |
| [<img src="https://avatars.githubusercontent.com/u/84392209?v=4" width="110px;"/><br /><sub>Toreg87</sub>](https://github.com/Toreg87)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Toreg87 "Code") | [<img src="https://avatars.githubusercontent.com/u/67638596?v=4" width="110px;"/><br /><sub>Matthew Nickson</sub>](https://github.com/Computroniks)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Computroniks "Code") | [<img src="https://avatars.githubusercontent.com/u/1646397?v=4" width="110px;"/><br /><sub>Jethro Nederhof</sub>](https://jethron.id.au)<br />[💻](https://github.com/snipe/snipe-it/commits?author=jethron "Code") | [<img src="https://avatars.githubusercontent.com/u/23289826?v=4" width="110px;"/><br /><sub>Oskar Stenberg</sub>](https://github.com/01ste02)<br />[💻](https://github.com/snipe/snipe-it/commits?author=01ste02 "Code") | [<img src="https://avatars.githubusercontent.com/u/82208283?v=4" width="110px;"/><br /><sub>Robert-Azelis</sub>](https://github.com/Robert-Azelis)<br />[💻](https://github.com/snipe/snipe-it/commits?author=Robert-Azelis "Code") | [<img src="https://avatars.githubusercontent.com/u/60648387?v=4" width="110px;"/><br /><sub>Alexander William Smith</sub>](https://github.com/alwism)<br />[💻](https://github.com/snipe/snipe-it/commits?author=alwism "Code") | [<img src="https://avatars.githubusercontent.com/u/24418301?v=4" width="110px;"/><br /><sub>LEITWERK AG</sub>](https://www.leitwerk.de/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=leitwerk-ag "Code") |
| [<img src="https://avatars.githubusercontent.com/u/1911435?v=4" width="110px;"/><br /><sub>Adam</sub>](http://www.aboutcher.co.uk)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adamboutcher "Code") | [<img src="https://avatars.githubusercontent.com/u/16104273?v=4" width="110px;"/><br /><sub>Ian</sub>](https://snksrv.com)<br />[💻](https://github.com/snipe/snipe-it/commits?author=sneak-it "Code") | [<img src="https://avatars.githubusercontent.com/u/4023909?v=4" width="110px;"/><br /><sub>Shao Yu-Lung (Allen)</sub>](http://blog.bestlong.idv.tw/)<br />[💻](https://github.com/snipe/snipe-it/commits?author=bestlong "Code") | [<img src="https://avatars.githubusercontent.com/u/88776392?v=4" width="110px;"/><br /><sub>PlaneNuts</sub>](https://github.com/PlaneNuts)<br />[💻](https://github.com/snipe/snipe-it/commits?author=PlaneNuts "Code") | [<img src="https://avatars.githubusercontent.com/u/3842948?v=4" width="110px;"/><br /><sub>Bradley Coudriet</sub>](http://bjcpgd.cias.rit.edu)<br />[💻](https://github.com/snipe/snipe-it/commits?author=exula "Code") | [<img src="https://avatars.githubusercontent.com/u/38761237?v=4" width="110px;"/><br /><sub>Alex Janes</sub>](https://adagiohealth.org)<br />[💻](https://github.com/snipe/snipe-it/commits?author=adagioajanes "Code") | [<img src="https://avatars.githubusercontent.com/u/32387849?v=4" width="110px;"/><br /><sub>Nuraeil</sub>](https://github.com/nuraeil)<br />[💻](https://github.com/snipe/snipe-it/commits?author=nuraeil "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!

View file

@ -91,7 +91,7 @@ class LdapSync extends Command
}
/* Determine which location to assign users to by default. */
$location = null; // FIXME - this would be better called "$default_location", which is more explicit about its purpose
$location = null; // TODO - this would be better called "$default_location", which is more explicit about its purpose
if ($this->option('location') != '') {
$location = Location::where('name', '=', $this->option('location'))->first();
@ -133,7 +133,7 @@ class LdapSync extends Command
foreach ($ldap_ou_locations as $ldap_loc) {
try {
$location_users = Ldap::findLdapUsers($ldap_loc['ldap_ou']);
} catch (\Exception $e) { // FIXME: this is stolen from line 77 or so above
} catch (\Exception $e) { // TODO: this is stolen from line 77 or so above
if ($this->option('json_summary')) {
$json_summary = ['error' => true, 'error_message' => trans('admin/users/message.error.ldap_could_not_search').' Location: '.$ldap_loc['name'].' (ID: '.$ldap_loc['id'].') cannot connect to "'.$ldap_loc['ldap_ou'].'" - '.$e->getMessage(), 'summary' => []];
$this->info(json_encode($json_summary));

View file

@ -1,398 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Adldap\Models\User as AdldapUser;
use App\Models\Location;
use App\Models\User;
use App\Services\LdapAd;
use Exception;
use Illuminate\Console\Command;
use Log;
/**
* LDAP / AD sync command.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
class LdapSyncNg extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'snipeit:ldap-sync-ng
{--location= : A location name }
{--location_id= : A location id}
{--base_dn= : A diffrent base DN to use }
{--summary : Print summary }
{--json_summary : Print summary in json format }
{--dryrun : Run the sync process but don\'t update the database}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command line LDAP/AD sync';
/**
* An LdapAd instance.
*
* @var \App\Models\LdapAd
*/
private $ldap;
/**
* LDAP settings collection.
*
* @var \Illuminate\Support\Collection
*/
private $settings = null;
/**
* A default location collection.
*
* @var \Illuminate\Support\Collection
*/
private $defaultLocation = null;
/**
* Mapped locations collection.
*
* @var \Illuminate\Support\Collection
*/
private $mappedLocations = null;
/**
* The summary collection.
*
* @var \Illuminate\Support\Collection
*/
private $summary;
/**
* Is dry-run?
*
* @var bool
*/
private $dryrun = false;
/**
* Show users to be imported.
*
* @var array
*/
private $userlist = [];
/**
* Create a new command instance.
*/
public function __construct(LdapAd $ldap)
{
parent::__construct();
$this->ldap = $ldap;
$this->settings = $this->ldap->ldapSettings;
$this->summary = collect();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$dispatcher = \Adldap\Adldap::getEventDispatcher();
// Listen for all model events.
$dispatcher->listen('Adldap\Models\Events\*', function ($eventName, array $data) {
echo $eventName; // Returns 'Adldap\Models\Events\Updating'
var_dump($data); // Returns [0] => (object) Adldap\Models\Events\Updating;
\Log::debug('Event: '.$eventName.' data - '.print_r($data, true));
});
$dispatcher->listen('Adldap\Auth\Events\*', function ($eventName, array $data) {
echo $eventName; // Returns 'Adldap\Models\Events\Updating'
var_dump($data); // Returns [0] => (object) Adldap\Models\Events\Updating;
\Log::debug('Event: '.$eventName.' data - '.print_r($data, true));
});
ini_set('max_execution_time', env('LDAP_TIME_LIM', '600')); //600 seconds = 10 minutes
ini_set('memory_limit', '500M');
$old_error_reporting = error_reporting(); // grab old error_reporting .ini setting, for later re-enablement
error_reporting($old_error_reporting & ~E_DEPRECATED); // disable deprecation warnings, for LDAP in PHP 7.4 (and greater)
if ($this->option('dryrun')) {
$this->dryrun = true;
}
$this->checkIfLdapIsEnabled();
$this->checkLdapConnection();
$this->setBaseDn();
$this->getUserDefaultLocation();
/*
* Use the default location if set, this is needed for the LDAP users sync page
*/
if (! $this->option('base_dn') && null == $this->defaultLocation) {
$this->getMappedLocations();
}
$this->processLdapUsers();
// Print table of users
if ($this->dryrun) {
$this->info('The following users will be synced!');
$headers = ['First Name', 'Last Name', 'Username', 'Email', 'Employee #', 'Location Id', 'Status'];
$this->table($headers, $this->summary->toArray());
}
error_reporting($old_error_reporting); // re-enable deprecation warnings.
return $this->getSummary();
}
/**
* Generate the LDAP sync summary.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return string
*/
private function getSummary(): string
{
if ($this->option('summary') && null === $this->dryrun) {
$this->summary->each(function ($item) {
$this->info('USER: '.$item['note']);
if ('ERROR' === $item['status']) {
$this->error('ERROR: '.$item['note']);
}
});
} elseif ($this->option('json_summary')) {
$json_summary = [
'error' => false,
'error_message' => '',
'summary' => $this->summary->toArray(),
];
$this->info(json_encode($json_summary));
}
return '';
}
/**
* Create a new user or update an existing user.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @param \Adldap\Models\User $snipeUser
*/
private function updateCreateUser(AdldapUser $snipeUser): void
{
$user = $this->ldap->processUser($snipeUser, $this->defaultLocation, $this->mappedLocations);
$summary = [
'firstname' => $user->first_name,
'lastname' => $user->last_name,
'username' => $user->username,
'employee_number' => $user->employee_num,
'email' => $user->email,
'location_id' => $user->location_id,
];
// Only update the database if is not a dry run
if (! $this->dryrun) {
if ($user->isDirty()) { //if nothing on the user changed, don't bother trying to save anything nor put anything in the summary
if ($user->save()) {
$summary['note'] = ($user->wasRecentlyCreated ? 'CREATED' : 'UPDATED');
$summary['status'] = 'SUCCESS';
} else {
$errors = '';
foreach ($user->getErrors()->getMessages() as $error) {
$errors .= implode(', ', $error);
}
$summary['note'] = $snipeUser->getDN().' was not imported. REASON: '.$errors;
$summary['status'] = 'ERROR';
}
} else {
$summary = null;
}
}
// $summary['note'] = ($user->getOriginal('username') ? 'UPDATED' : 'CREATED'); // this seems, kinda, like, superfluous, relative to the $summary['note'] thing above, yeah?
if ($summary) { //if the $user wasn't dirty, $summary was set to null so that we will skip the following push()
$this->summary->push($summary);
}
}
/**
* Process the users to update / create.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function processLdapUsers(): void
{
try {
\Log::debug('CAL:LING GET LDAP SUSERS');
$ldapUsers = $this->ldap->getLdapUsers();
\Log::debug('END CALLING GET LDAP USERS');
} catch (Exception $e) {
$this->outputError($e);
exit($e->getMessage());
}
if (0 == $ldapUsers->count()) {
$msg = 'ERROR: No users found!';
Log::error($msg);
if ($this->dryrun) {
$this->error($msg);
}
exit($msg);
}
// Process each individual users
foreach ($ldapUsers->getResults() as $user) { // AdLdap2's paginate() method is weird, it gets *everything* and ->getResults() returns *everything*
$this->updateCreateUser($user);
}
}
/**
* Get the mapped locations if a base_dn is provided.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function getMappedLocations()
{
$ldapOuLocation = Location::where('ldap_ou', '!=', '')->select(['id', 'ldap_ou'])->get();
$locations = $ldapOuLocation->sortBy(function ($ou, $key) {
return strlen($ou->ldap_ou);
});
if ($locations->count() > 0) {
$msg = 'Some locations have special OUs set. Locations will be automatically set for users in those OUs.';
LOG::debug($msg);
if ($this->dryrun) {
$this->info($msg);
}
$this->mappedLocations = $locations->pluck('ldap_ou', 'id'); // TODO: this seems ok-ish, but the key-> value is going location_id -> OU name, and the primary action here is the opposite of that - going from OU's to location ID's.
}
}
/**
* Set the base dn if supplied.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function setBaseDn(): void
{
if ($this->option('base_dn')) {
$this->ldap->baseDn = $this->option('base_dn');
$msg = sprintf('Importing users from specified base DN: "%s"', $this->ldap->baseDn);
LOG::debug($msg);
if ($this->dryrun) {
$this->info($msg);
}
}
}
/**
* Get a default location id for imported users.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function getUserDefaultLocation(): void
{
$location = $this->option('location_id') ?? $this->option('location');
if ($location) {
$userLocation = Location::where('name', '=', $location)
->orWhere('id', '=', intval($location))
->select(['name', 'id'])
->first();
if ($userLocation) {
$msg = 'Importing users with default location: '.$userLocation->name.' ('.$userLocation->id.')';
LOG::debug($msg);
if ($this->dryrun) {
$this->info($msg);
}
$this->defaultLocation = collect([
$userLocation->id => $userLocation->name,
]);
} else {
$msg = 'The supplied location is invalid!';
LOG::error($msg);
if ($this->dryrun) {
$this->error($msg);
}
exit(0);
}
}
}
/**
* Check if LDAP intergration is enabled.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function checkIfLdapIsEnabled(): void
{
if (false === $this->settings['ldap_enabled']) {
$msg = 'LDAP intergration is not enabled. Exiting sync process.';
$this->info($msg);
Log::info($msg);
exit(0);
}
}
/**
* Check to make sure we can access the server.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function checkLdapConnection(): void
{
try {
$this->ldap->testLdapAdUserConnection();
$this->ldap->testLdapAdBindConnection();
} catch (Exception $e) {
$this->outputError($e);
exit(0);
}
}
/**
* Output the json summary to the screen if enabled.
*
* @param Exception $error
*/
private function outputError($error): void
{
if ($this->option('json_summary')) {
$json_summary = [
'error' => true,
'error_message' => $error->getMessage(),
'summary' => [],
];
$this->info(json_encode($json_summary));
}
$this->error($error->getMessage());
LOG::error($error);
}
}

View file

@ -26,7 +26,7 @@ class LicensesController extends Controller
public function index(Request $request)
{
$this->authorize('view', License::class);
$licenses = Company::scopeCompanyables(License::with('company', 'manufacturer', 'freeSeats', 'supplier', 'category')->withCount('freeSeats as free_seats_count'));
$licenses = Company::scopeCompanyables(License::with('company', 'manufacturer', 'supplier', 'category')->withCount('freeSeats as free_seats_count'));
if ($request->filled('company_id')) {
$licenses->where('company_id', '=', $request->input('company_id'));
@ -144,7 +144,6 @@ class LicensesController extends Controller
}
$total = $licenses->count();
$licenses = $licenses->skip($offset)->take($limit)->get();
return (new LicensesTransformer)->transformLicenses($licenses, $total);

View file

@ -88,7 +88,7 @@ class PredefinedKitsController extends Controller
$kit->fill($request->all());
if ($kit->save()) {
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.update_success'))); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.update_success')));
}
return response()->json(Helper::formatStandardApiResponse('error', null, $kit->getErrors()));
@ -113,7 +113,7 @@ class PredefinedKitsController extends Controller
$kit->delete();
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/kits/general.delete_success'))); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/kits/general.delete_success')));
}
/**
@ -171,12 +171,12 @@ class PredefinedKitsController extends Controller
$license_id = $request->get('license');
$relation = $kit->licenses();
if ($relation->find($license_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, ['license' => 'License already attached to kit']));
return response()->json(Helper::formatStandardApiResponse('error', null, ['license' => trans('admin/kits/general.license_error')]));
}
$relation->attach($license_id, ['quantity' => $quantity]);
return response()->json(Helper::formatStandardApiResponse('success', $kit, 'License added successfull')); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.license_added_success')));
}
/**
@ -196,7 +196,7 @@ class PredefinedKitsController extends Controller
}
$kit->licenses()->syncWithoutDetaching([$license_id => ['quantity' => $quantity]]);
return response()->json(Helper::formatStandardApiResponse('success', $kit, 'License updated')); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.license_updated')));
}
/**
@ -274,7 +274,7 @@ class PredefinedKitsController extends Controller
}
$kit->models()->syncWithoutDetaching([$model_id => ['quantity' => $quantity]]);
return response()->json(Helper::formatStandardApiResponse('success', $kit, 'License updated')); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.license_updated')));
}
/**
@ -327,12 +327,12 @@ class PredefinedKitsController extends Controller
$consumable_id = $request->get('consumable');
$relation = $kit->consumables();
if ($relation->find($consumable_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, ['consumable' => 'Consumable already attached to kit']));
return response()->json(Helper::formatStandardApiResponse('error', null, ['consumable' => trans('admin/kits/general.consumable_error')]));
}
$relation->attach($consumable_id, ['quantity' => $quantity]);
return response()->json(Helper::formatStandardApiResponse('success', $kit, 'Consumable added successfull')); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.consumable_added_success')));
}
/**
@ -352,7 +352,7 @@ class PredefinedKitsController extends Controller
}
$kit->consumables()->syncWithoutDetaching([$consumable_id => ['quantity' => $quantity]]);
return response()->json(Helper::formatStandardApiResponse('success', $kit, 'Consumable updated')); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.consumable_updated')));
}
/**
@ -368,7 +368,7 @@ class PredefinedKitsController extends Controller
$kit->consumables()->detach($consumable_id);
return response()->json(Helper::formatStandardApiResponse('success', $kit, 'Delete was successfull')); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.consumable_deleted')));
}
/**
@ -405,12 +405,12 @@ class PredefinedKitsController extends Controller
$accessory_id = $request->get('accessory');
$relation = $kit->accessories();
if ($relation->find($accessory_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, ['accessory' => 'Accessory already attached to kit']));
return response()->json(Helper::formatStandardApiResponse('error', null, ['accessory' => trans('admin/kits/general.accessory_error')]));
}
$relation->attach($accessory_id, ['quantity' => $quantity]);
return response()->json(Helper::formatStandardApiResponse('success', $kit, 'Accessory added successfull')); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.accessory_added_success')));
}
/**
@ -430,7 +430,7 @@ class PredefinedKitsController extends Controller
}
$kit->accessories()->syncWithoutDetaching([$accessory_id => ['quantity' => $quantity]]);
return response()->json(Helper::formatStandardApiResponse('success', $kit, 'Accessory updated')); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.accessory_updated')));
}
/**
@ -446,6 +446,6 @@ class PredefinedKitsController extends Controller
$kit->accessories()->detach($accessory_id);
return response()->json(Helper::formatStandardApiResponse('success', $kit, 'Delete was successfull')); // TODO: trans
return response()->json(Helper::formatStandardApiResponse('success', $kit, trans('admin/kits/general.accessory_deleted')));
}
}

View file

@ -2,15 +2,15 @@
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Transformers\LoginAttemptsTransformer;
use App\Models\Ldap;
use App\Models\Setting;
use Mail;
use App\Notifications\SlackTest;
use App\Notifications\MailTest;
use App\Services\LdapAd;
use GuzzleHttp\Client;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@ -22,105 +22,69 @@ use App\Http\Requests\SlackSettingsRequest;
class SettingsController extends Controller
{
/**
* Test the ldap settings
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @param App\Models\LdapAd $ldap
*
* @return \Illuminate\Http\JsonResponse
*/
public function ldapAdSettingsTest(LdapAd $ldap): JsonResponse
{
if (! $ldap->init()) {
Log::info('LDAP is not enabled so we cannot test.');
public function ldaptest()
{
$settings = Setting::getSettings();
if ($settings->ldap_enabled!='1') {
\Log::debug('LDAP is not enabled cannot test.');
return response()->json(['message' => 'LDAP is not enabled, cannot test.'], 400);
}
// The connect, bind and resulting users message
$message = [];
\Log::debug('Preparing to test LDAP connection');
// This is all kinda fucked right now. The connection test doesn't actually do what you think,
// // and the way we parse the errors
// on the JS side is horrible.
Log::info('Preparing to test LDAP user login');
// Test user can connect to the LDAP server
$message = []; //where we collect together test messages
try {
$ldap->testLdapAdUserConnection();
$message['login'] = [
'message' => 'Successfully connected to LDAP server.',
];
} catch (\Exception $ex) {
\Log::debug('Connection to LDAP server '.Setting::getSettings()->ldap_server.' failed. Please check your LDAP settings and try again. Server Responded with error: '.$ex->getMessage());
return response()->json(
['message' => 'Connection to LDAP server '.Setting::getSettings()->ldap_server." failed. Verify that the LDAP hostname is entered correctly and that it can be reached from this web server. \n\nServer Responded with error: ".$ex->getMessage(),
], 400);
}
Log::info('Preparing to test LDAP bind connection');
// Test user can bind to the LDAP server
try {
Log::info('Testing Bind');
$ldap->testLdapAdBindConnection();
$message['bind'] = [
'message' => 'Successfully bound to LDAP server.',
];
} catch (\Exception $ex) {
Log::info('LDAP Bind failed');
return response()->json(['message' => 'Connection to LDAP successful, but we were unable to Bind the LDAP user '.Setting::getSettings()->ldap_uname.". Verify your that your LDAP Bind username and password are correct. \n\nServer Responded with error: ".$ex->getMessage(),
], 400);
}
Log::info('Preparing to get sample user set from LDAP directory');
// Get a sample of 10 users so user can verify the data is correct
$settings = Setting::getSettings();
try {
Log::info('Testing LDAP sync');
error_reporting(E_ALL & ~E_DEPRECATED); // workaround for php7.4, which deprecates ldap_control_paged_result
// $users = $ldap->testUserImportSync(); // from AdLdap2 from v5, disabling and falling back to v4's sync code
$users = collect(Ldap::findLdapUsers())->slice(0, 11)->filter(function ($value, $key) { //choosing ELEVEN because one is going to be the count, which we're about to filter out in the next line
return is_int($key);
})->map(function ($item) use ($settings) {
return (object) [
'username' => $item[$settings['ldap_username_field']][0] ?? null,
'employee_number' => $item[$settings['ldap_emp_num']][0] ?? null,
'lastname' => $item[$settings['ldap_lname_field']][0] ?? null,
'firstname' => $item[$settings['ldap_fname_field']][0] ?? null,
'email' => $item[$settings['ldap_email']][0] ?? null,
];
});
if ($users->count() > 0) {
$message['user_sync'] = [
'users' => $users,
];
} else {
$message['user_sync'] = [
'message' => 'Connection to LDAP was successful, however there were no users returned from your query. You should confirm the Base Bind DN above.',
$connection = Ldap::connectToLdap();
try {
$message['bind'] = ['message' => 'Successfully bound to LDAP server.'];
\Log::debug('attempting to bind to LDAP for LDAP test');
Ldap::bindAdminToLdap($connection);
$message['login'] = [
'message' => 'Successfully connected to LDAP server.',
];
return response()->json($message, 400);
$users = collect(Ldap::findLdapUsers(null,10))->filter(function ($value, $key) {
return is_int($key);
})->slice(0, 10)->map(function ($item) use ($settings) {
return (object) [
'username' => $item[$settings['ldap_username_field']][0] ?? null,
'employee_number' => $item[$settings['ldap_emp_num']][0] ?? null,
'lastname' => $item[$settings['ldap_lname_field']][0] ?? null,
'firstname' => $item[$settings['ldap_fname_field']][0] ?? null,
'email' => $item[$settings['ldap_email']][0] ?? null,
];
});
if ($users->count() > 0) {
$message['user_sync'] = [
'users' => $users,
];
} else {
$message['user_sync'] = [
'message' => 'Connection to LDAP was successful, however there were no users returned from your query. You should confirm the Base Bind DN above.',
];
return response()->json($message, 400);
}
return response()->json($message, 200);
} catch (\Exception $e) {
\Log::debug('Bind failed');
\Log::debug("Exception was: ".$e->getMessage());
return response()->json(['message' => $e->getMessage()], 400);
//return response()->json(['message' => $e->getMessage()], 500);
}
} catch (\Exception $ex) {
Log::info('LDAP sync failed');
$message['user_sync'] = [
'message' => 'Error getting users from LDAP directory, error: '.$ex->getMessage(),
];
return response()->json($message, 400);
} catch (\Exception $e) {
\Log::debug('Connection failed but we cannot debug it any further on our end.');
return response()->json(['message' => $e->getMessage()], 500);
}
return response()->json($message, 200);
}
public function ldaptestlogin(Request $request, LdapAd $ldap)
public function ldaptestlogin(Request $request)
{
if (Setting::getSettings()->ldap_enabled != '1') {
@ -129,40 +93,50 @@ class SettingsController extends Controller
}
$rules = [
$rules = array(
'ldaptest_user' => 'required',
'ldaptest_password' => 'required',
];
'ldaptest_password' => 'required'
);
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
\Log::debug('LDAP Validation test failed.');
$validation_errors = implode(' ', $validator->errors()->all());
$validation_errors = implode(' ',$validator->errors()->all());
return response()->json(['message' => $validator->errors()->all()], 400);
}
\Log::debug('Preparing to test LDAP login');
try {
DB::beginTransaction(); //this was the easiest way to invoke a full test of an LDAP login without adding new users to the DB (which may not be desired)
$connection = Ldap::connectToLdap();
try {
Ldap::bindAdminToLdap($connection);
\Log::debug('Attempting to bind to LDAP for LDAP test');
try {
$ldap_user = Ldap::findAndBindUserLdap($request->input('ldaptest_user'), $request->input('ldaptest_password'));
if ($ldap_user) {
\Log::debug('It worked! '. $request->input('ldaptest_user').' successfully binded to LDAP.');
return response()->json(['message' => 'It worked! '. $request->input('ldaptest_user').' successfully binded to LDAP.'], 200);
}
return response()->json(['message' => 'Login Failed. '. $request->input('ldaptest_user').' did not successfully bind to LDAP.'], 400);
// $results = $ldap->ldap->auth()->attempt($request->input('ldaptest_username'), $request->input('ldaptest_password'), true);
// can't do this because that's a protected property.
} catch (\Exception $e) {
\Log::debug('LDAP login failed');
return response()->json(['message' => $e->getMessage()], 400);
}
$results = $ldap->ldapLogin($request->input('ldaptest_user'), $request->input('ldaptest_password')); // this would normally create a user on success (if they didn't already exist), but for the transaction
if ($results) {
return response()->json(['message' => 'It worked! '.$request->input('ldaptest_user').' successfully binded to LDAP.'], 200);
} else {
return response()->json(['message' => 'Login Failed. '.$request->input('ldaptest_user').' did not successfully bind to LDAP.'], 400);
} catch (\Exception $e) {
\Log::debug('Bind failed');
return response()->json(['message' => $e->getMessage()], 400);
//return response()->json(['message' => $e->getMessage()], 500);
}
} catch (\Exception $e) {
\Log::debug('Connection failed');
return response()->json(['message' => $e->getMessage()], 400);
} finally {
DB::rollBack(); // ALWAYS rollback, whether success or failure
return response()->json(['message' => $e->getMessage()], 500);
}
}
public function slacktest(SlackSettingsRequest $request)
@ -215,10 +189,9 @@ class SettingsController extends Controller
*/
public function ajaxTestEmail()
{
if (! config('app.lock_passwords')) {
if (!config('app.lock_passwords')) {
try {
Notification::send(Setting::first(), new MailTest());
return response()->json(['message' => 'Mail sent to '.config('mail.reply_to.address')], 200);
} catch (\Exception $e) {
return response()->json(['message' => $e->getMessage()], 500);
@ -291,4 +264,4 @@ class SettingsController extends Controller
return (new LoginAttemptsTransformer)->transformLoginAttempts($login_attempt_results, $total);
}
}
}

View file

@ -5,7 +5,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use App\Models\User;
use App\Services\LdapAd;
use App\Models\Ldap;
use App\Services\Saml;
use Com\Tecnick\Barcode\Barcode;
use Google2FA;
@ -39,11 +39,6 @@ class LoginController extends Controller
*/
protected $redirectTo = '/';
/**
* @var LdapAd
*/
protected $ldap;
/**
* @var Saml
*/
@ -52,12 +47,11 @@ class LoginController extends Controller
/**
* Create a new authentication controller instance.
*
* @param LdapAd $ldap
* @param Saml $saml
*
* @return void
*/
public function __construct(/*LdapAd $ldap, */ Saml $saml)
public function __construct(Saml $saml)
{
parent::__construct();
$this->middleware('guest', ['except' => ['logout', 'postTwoFactorAuth', 'getTwoFactorAuth', 'getTwoFactorEnroll']]);
@ -147,13 +141,47 @@ class LoginController extends Controller
*/
private function loginViaLdap(Request $request): User
{
$ldap = \App::make(LdapAd::class);
try {
return $ldap->ldapLogin($request->input('username'), $request->input('password'));
} catch (\Exception $ex) {
LOG::debug('LDAP user login: '.$ex->getMessage());
throw new \Exception($ex->getMessage());
}
Log::debug("Binding user to LDAP.");
$ldap_user = Ldap::findAndBindUserLdap($request->input('username'), $request->input('password'));
if (!$ldap_user) {
Log::debug("LDAP user ".$request->input('username')." not found in LDAP or could not bind");
throw new \Exception("Could not find user in LDAP directory");
} else {
Log::debug("LDAP user ".$request->input('username')." successfully bound to LDAP");
}
// Check if the user already exists in the database and was imported via LDAP
$user = User::where('username', '=', $request->input('username'))->whereNull('deleted_at')->where('ldap_import', '=', 1)->where('activated', '=', '1')->first(); // FIXME - if we get more than one we should fail. and we sure about this ldap_import thing?
Log::debug("Local auth lookup complete");
// The user does not exist in the database. Try to get them from LDAP.
// If user does not exist and authenticates successfully with LDAP we
// will create it on the fly and sign in with default permissions
if (!$user) {
Log::debug("Local user ".$request->input('username')." does not exist");
Log::debug("Creating local user ".$request->input('username'));
if ($user = Ldap::createUserFromLdap($ldap_user)) { //this handles passwords on its own
Log::debug("Local user created.");
} else {
Log::debug("Could not create local user.");
throw new \Exception("Could not create local user");
}
// If the user exists and they were imported from LDAP already
} else {
Log::debug("Local user ".$request->input('username')." exists in database. Updating existing user against LDAP.");
$ldap_attr = Ldap::parseAndMapLdapAttributes($ldap_user);
if (Setting::getSettings()->ldap_pw_sync=='1') {
$user->password = bcrypt($request->input('password'));
}
$user->email = $ldap_attr['email'];
$user->first_name = $ldap_attr['firstname'];
$user->last_name = $ldap_attr['lastname']; //FIXME (or TODO?) - do we need to map additional fields that we now support? E.g. country, phone, etc.
$user->save();
} // End if(!user)
return $user;
}
private function loginViaRemoteUser(Request $request)

View file

@ -61,12 +61,12 @@ 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', 'Checkout error')->with('error_messages', $checkout_result['errors']); // TODO: trans
return redirect()->back()->with('error', trans('general.checkout_error'))->with('error_messages', $checkout_result['errors']);
}
return redirect()->back()->with('success', 'Checkout was successful')
return redirect()->back()->with('success', trans('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)); // TODO: trans
->with('consumables', Arr::get($checkout_result, 'consumables', null));
}
}

View file

@ -64,7 +64,7 @@ class PredefinedKitsController extends Controller
return redirect()->back()->withInput()->withErrors($kit->getErrors());
}
return redirect()->route('kits.index')->with('success', 'Kit was successfully created.'); // TODO: trans()
return redirect()->route('kits.index')->with('success', trans('admin/kits/general.kit_created'));
}
/**
@ -85,7 +85,7 @@ class PredefinedKitsController extends Controller
->with('licenses', $kit->licenses);
}
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
/**
@ -103,13 +103,13 @@ class PredefinedKitsController extends Controller
// Check if the kit exists
if (is_null($kit = PredefinedKit::find($kit_id))) {
// Redirect to the kits management page
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
$kit->name = $request->input('name');
if ($kit->save()) {
return redirect()->route('kits.index')->with('success', 'Kit was successfully updated'); // TODO: trans
return redirect()->route('kits.index')->with('success', trans('admin/kits/general.kit_updated'));
}
return redirect()->back()->withInput()->withErrors($kit->getErrors());
@ -129,7 +129,7 @@ class PredefinedKitsController extends Controller
$this->authorize('delete', PredefinedKit::class);
// Check if the kit exists
if (is_null($kit = PredefinedKit::find($kit_id))) {
return redirect()->route('kits.index')->with('error', 'Kit not found'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_not_found'));
}
// Delete childs
@ -141,7 +141,7 @@ class PredefinedKitsController extends Controller
$kit->delete();
// Redirect to the kit management page
return redirect()->route('kits.index')->with('success', 'Kit was successfully deleted'); // TODO: trans
return redirect()->route('kits.index')->with('success', trans('admin/kits/general.kit_deleted'));
}
/**
@ -176,7 +176,7 @@ class PredefinedKitsController extends Controller
]);
}
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
/**
@ -191,7 +191,7 @@ class PredefinedKitsController extends Controller
$this->authorize('update', PredefinedKit::class);
if (is_null($kit = PredefinedKit::find($kit_id))) {
// Redirect to the kits management page
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
$validator = \Validator::make($request->all(), $kit->makeModelRules($model_id));
@ -206,7 +206,7 @@ class PredefinedKitsController extends Controller
$pivot->quantity = $request->input('quantity');
$pivot->save();
return redirect()->route('kits.edit', $kit_id)->with('success', 'Model updated successfully.'); // TODO: trans
return redirect()->route('kits.edit', $kit_id)->with('success', trans('admin/kits/general.kit_model_updated'));
}
/**
@ -221,14 +221,14 @@ class PredefinedKitsController extends Controller
$this->authorize('update', PredefinedKit::class);
if (is_null($kit = PredefinedKit::find($kit_id))) {
// Redirect to the kits management page
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
// Delete childs
$kit->models()->detach($model_id);
// Redirect to the kit management page
return redirect()->route('kits.edit', $kit_id)->with('success', 'Model was successfully detached'); // TODO: trans
return redirect()->route('kits.edit', $kit_id)->with('success', trans('admin/kits/general.kit_model_detached'));
}
/**
@ -243,10 +243,10 @@ class PredefinedKitsController extends Controller
{
$this->authorize('update', PredefinedKit::class);
if (! ($kit = PredefinedKit::find($kit_id))) {
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
if (! ($license = $kit->licenses()->find($license_id))) {
return redirect()->route('kits.index')->with('error', 'License does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.license_none'));
}
return view('kits/license-edit', [
@ -269,7 +269,7 @@ class PredefinedKitsController extends Controller
$this->authorize('update', PredefinedKit::class);
if (is_null($kit = PredefinedKit::find($kit_id))) {
// Redirect to the kits management page
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
$validator = \Validator::make($request->all(), $kit->makeLicenseRules($license_id));
@ -284,7 +284,7 @@ class PredefinedKitsController extends Controller
$pivot->quantity = $request->input('quantity');
$pivot->save();
return redirect()->route('kits.edit', $kit_id)->with('success', 'License updated successfully.'); // TODO: trans
return redirect()->route('kits.edit', $kit_id)->with('success', trans('admin/kits/general.license_updated'));
}
/**
@ -300,14 +300,14 @@ class PredefinedKitsController extends Controller
$this->authorize('update', PredefinedKit::class);
if (is_null($kit = PredefinedKit::find($kit_id))) {
// Redirect to the kits management page
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
// Delete childs
$kit->licenses()->detach($license_id);
// Redirect to the kit management page
return redirect()->route('kits.edit', $kit_id)->with('success', 'License was successfully detached'); // TODO: trans
return redirect()->route('kits.edit', $kit_id)->with('success', trans('admin/kits/general.license_detached'));
}
/**
@ -322,10 +322,10 @@ class PredefinedKitsController extends Controller
{
$this->authorize('update', PredefinedKit::class);
if (! ($kit = PredefinedKit::find($kit_id))) {
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
if (! ($accessory = $kit->accessories()->find($accessory_id))) {
return redirect()->route('kits.index')->with('error', 'Accessory does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.accessory_none'));
}
return view('kits/accessory-edit', [
@ -348,7 +348,7 @@ class PredefinedKitsController extends Controller
$this->authorize('update', PredefinedKit::class);
if (is_null($kit = PredefinedKit::find($kit_id))) {
// Redirect to the kits management page
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
$validator = \Validator::make($request->all(), $kit->makeAccessoryRules($accessory_id));
@ -363,7 +363,7 @@ class PredefinedKitsController extends Controller
$pivot->quantity = $request->input('quantity');
$pivot->save();
return redirect()->route('kits.edit', $kit_id)->with('success', 'Accessory updated successfully.'); // TODO: trans
return redirect()->route('kits.edit', $kit_id)->with('success', trans('admin/kits/general.accessory_updated'));
}
/**
@ -378,14 +378,14 @@ class PredefinedKitsController extends Controller
$this->authorize('update', PredefinedKit::class);
if (is_null($kit = PredefinedKit::find($kit_id))) {
// Redirect to the kits management page
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
// Delete childs
$kit->accessories()->detach($accessory_id);
// Redirect to the kit management page
return redirect()->route('kits.edit', $kit_id)->with('success', 'Accessory was successfully detached'); // TODO: trans
return redirect()->route('kits.edit', $kit_id)->with('success', trans('admin/kits/general.accessory_detached'));
}
/**
@ -400,10 +400,10 @@ class PredefinedKitsController extends Controller
{
$this->authorize('update', PredefinedKit::class);
if (! ($kit = PredefinedKit::find($kit_id))) {
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
if (! ($consumable = $kit->consumables()->find($consumable_id))) {
return redirect()->route('kits.index')->with('error', 'Consumable does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.consumable_none'));
}
return view('kits/consumable-edit', [
@ -426,7 +426,7 @@ class PredefinedKitsController extends Controller
$this->authorize('update', PredefinedKit::class);
if (is_null($kit = PredefinedKit::find($kit_id))) {
// Redirect to the kits management page
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
$validator = \Validator::make($request->all(), $kit->makeConsumableRules($consumable_id));
@ -441,7 +441,7 @@ class PredefinedKitsController extends Controller
$pivot->quantity = $request->input('quantity');
$pivot->save();
return redirect()->route('kits.edit', $kit_id)->with('success', 'Consumable updated successfully.'); // TODO: trans
return redirect()->route('kits.edit', $kit_id)->with('success', trans('admin/kits/general.consumable_updated'));
}
/**
@ -456,13 +456,13 @@ class PredefinedKitsController extends Controller
$this->authorize('update', PredefinedKit::class);
if (is_null($kit = PredefinedKit::find($kit_id))) {
// Redirect to the kits management page
return redirect()->route('kits.index')->with('error', 'Kit does not exist'); // TODO: trans
return redirect()->route('kits.index')->with('error', trans('admin/kits/general.kit_none'));
}
// Delete childs
$kit->consumables()->detach($consumable_id);
// Redirect to the kit management page
return redirect()->route('kits.edit', $kit_id)->with('success', 'Consumable was successfully detached'); // TODO: trans
return redirect()->route('kits.edit', $kit_id)->with('success', trans('admin/kits/general.consumable_detached'));
}
}

View file

@ -230,7 +230,7 @@ class LicensesController extends Controller
*/
public function show($licenseId = null)
{
$license = License::with('assignedusers', 'licenseSeats.user', 'licenseSeats.asset')->find($licenseId);
$license = License::with('assignedusers')->find($licenseId);
if ($license) {
$this->authorize('view', $license);

View file

@ -4,32 +4,12 @@ namespace App\Http\Controllers\Users;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\LdapAd;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan; // Note that this is awful close to 'Users' the namespace above; be careful
class LDAPImportController extends Controller
{
/**
* An Ldap instance.
*
* @var LdapAd
*/
protected $ldap;
/**
* __construct.
*
* @param LdapAd $ldap
*/
public function __construct(LdapAd $ldap)
{
parent::__construct();
$this->ldap = $ldap;
$this->ldap->init();
}
/**
/**
* Return view for LDAP import.
*
* @author Aladin Alaily
@ -43,6 +23,7 @@ class LDAPImportController extends Controller
*/
public function create()
{
// I guess this prolly oughtta... I dunno. Do something?
$this->authorize('update', User::class);
try {
//$this->ldap->connect(); I don't think this actually exists in LdapAd.php, and we don't really 'persist' LDAP connections anyways...right?

View file

@ -529,7 +529,7 @@ class UsersController extends Controller
strtolower(trans('general.id')),
trans('admin/companies/table.title'),
trans('admin/users/table.title'),
trans('admin/users/table.employee_num'),
trans('general.employee_number'),
trans('admin/users/table.name'),
trans('admin/users/table.username'),
trans('admin/users/table.email'),

View file

@ -38,11 +38,14 @@ class LoginForm extends Component
$this->can_submit = false;
}
$this->validateOnly($fields);
$this->can_submit = true;
$whatever = $this->validateOnly($fields);
//\Log::info(print_r($whatever,true));
$errors = $this->getErrorBag();
$this->can_submit = $this->username !== "" && $this->password !== "" && !$errors->has('username') && !$errors->has('password') ; // wait, what?
\Log::info("Oy - can we submit yet?!".$this->can_submit);
}
/**
@ -58,7 +61,7 @@ class LoginForm extends Component
public function submitForm()
{
$this->can_submit = true;
//$this->can_submit = true;
if (auth()->attempt($this->validate())) {
return redirect()->intended('/');

View file

@ -9,6 +9,22 @@ use Illuminate\Database\Eloquent\Model;
use Input;
use Log;
/***********************************************
* TODOS:
*
* First off, we should probably make it so that the main LDAP thing we're using is an *instance* of this class,
* rather than the static methods we use here. We should probably load up that class with its settings, so we
* don't have to explicitly refer to them so often.
*
* Then, we should probably look at embedding some of the logic we use elsewhere into here - the various methods
* should either return a User or false, or other things like that. Don't make the consumers of this class reach
* into its guts. While that conflates this model with the User model, I think having the appropriate logic for
* turning LDAP people into Users ought to belong here, so it's easier on the consumer of this class.
*
* We're probably going to have to eventually make it so that Snipe-IT users can define multiple LDAP servers,
* and having this as a more instance-oriented class will be a step in the right direction.
***********************************************/
class Ldap extends Model
{
/**
@ -87,22 +103,40 @@ class Ldap extends Model
if ($ldap_username_field == 'userprincipalname') {
$userDn = $username;
} else {
// In case they haven't added an AD domain
// TODO - we no longer respect the "add AD Domain to username" checkbox, but it still exists in settings.
// We should probably just eliminate that checkbox to avoid confusion.
// We let it sit in the DB, unused, to facilitate people downgrading (if they decide to).
// Hopefully, in a later release, we can remove it from the settings.
// This logic instead just means that if we're using UPN, we don't append ad_domain, if we aren't, then we do.
// Hopefully that should handle all of our use cases, but if not we can backport our old logic.
$userDn = ($settings->ad_domain != '') ? $username.'@'.$settings->ad_domain : $username.'@'.$settings->email_domain;
}
}
\Log::debug('Attempting to login using distinguished name:'.$userDn);
$filterQuery = $settings->ldap_auth_filter_query.$username;
$filter = Setting::getSettings()->ldap_filter;
$filter = Setting::getSettings()->ldap_filter; //FIXME - this *does* respect the ldap filter, but I believe that AdLdap2 did *not*.
$filterQuery = "({$filter}({$filterQuery}))";
\Log::debug('Filter query: '.$filterQuery);
if (! $ldapbind = @ldap_bind($connection, $userDn, $password)) {
\Log::debug("Status of binding user: $userDn to directory: (directly!) ".($ldapbind ? "success" : "FAILURE"));
if (! $ldapbind = self::bindAdminToLdap($connection)) {
return false;
/*
* TODO PLEASE:
*
* this isn't very clear, so it's important to note: the $ldapbind value is never correctly returned - we never 'return true' from self::bindAdminToLdap() (the function
* just "falls off the end" without ever explictly returning 'true')
*
* but it *does* have an interesting side-effect of checking for the LDAP password being incorrectly encrypted with the wrong APP_KEY, so I'm leaving it in for now.
*
* If it *did* correctly return 'true' on a succesful bind, it would _probably_ allow users to log in with an incorrect password. Which would be horrible!
*
* Let's definitely fix this at the next refactor!!!!
*
*/
\Log::debug("Status of binding Admin user: $userDn to directory instead: ".($ldapbind ? "success" : "FAILURE"));
return false;
}
}
@ -135,8 +169,6 @@ class Ldap extends Model
{
$ldap_username = Setting::getSettings()->ldap_uname;
$ldap_username = Setting::getSettings()->ldap_uname;
// Lets return some nicer messages for users who donked their app key, and disable LDAP
try {
$ldap_pass = \Crypt::decrypt(Setting::getSettings()->ldap_pword);
@ -147,6 +179,11 @@ class Ldap extends Model
if (! $ldapbind = @ldap_bind($connection, $ldap_username, $ldap_pass)) {
throw new Exception('Could not bind to LDAP: '.ldap_error($connection));
}
// TODO - this just "falls off the end" but the function states that it should return true or false
// unfortunately, one of the use cases for this function is wrong and *needs* for that failure mode to fire
// so I don't want to fix this right now.
// this method MODIFIES STATE on the passed-in $connection and just returns true or false (or, in this case, undefined)
// at the next refactor, this should be appropriately modified to be more consistent.
}
@ -233,14 +270,14 @@ class Ldap extends Model
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v3.0]
* @param $ldapatttibutes
* @param $base_dn
* @param $count
* @return array|bool
*/
public static function findLdapUsers($base_dn = null)
public static function findLdapUsers($base_dn = null, $count = -1)
{
$ldapconn = self::connectToLdap();
$ldap_bind = self::bindAdminToLdap($ldapconn);
self::bindAdminToLdap($ldapconn);
// Default to global base DN if nothing else is provided.
if (is_null($base_dn)) {
$base_dn = Setting::getSettings()->ldap_basedn;
@ -256,40 +293,58 @@ class Ldap extends Model
// Perform the search
do {
// Paginate (non-critical, if not supported by server)
if (! $ldap_paging = @ldap_control_paged_result($ldapconn, $page_size, false, $cookie)) {
throw new Exception('Problem with your LDAP connection. Try checking the Use TLS setting in Admin > Settings. ');
}
if ($filter != '' && substr($filter, 0, 1) != '(') { // wrap parens around NON-EMPTY filters that DON'T have them, for back-compatibility with AdLdap2-based filters
$filter = "($filter)";
} elseif ($filter == '') {
$filter = '(cn=*)';
}
$search_results = ldap_search($ldapconn, $base_dn, $filter);
// HUGE thanks to this article: https://stackoverflow.com/questions/68275972/how-to-get-paged-ldap-queries-in-php-8-and-read-more-than-1000-entries
// which helped me wrap my head around paged results!
\Log::info("ldap conn is: ".$ldapconn." basedn is: $base_dn, filter is: $filter - count is: $count. page size is: $page_size"); //FIXME - remove
// if a $count is set and it's smaller than $page_size then use that as the page size
$ldap_controls = [];
//if($count == -1) { //count is -1 means we have to employ paging to query the entire directory
$ldap_controls = [['oid' => LDAP_CONTROL_PAGEDRESULTS, 'iscritical' => false, 'value' => ['size'=> $count == -1||$count>$page_size ? $page_size : $count, 'cookie' => $cookie]]];
//}
$search_results = ldap_search($ldapconn, $base_dn, $filter, [], 0, /* $page_size */ -1, -1, LDAP_DEREF_NEVER, $ldap_controls); // TODO - I hate the @, and I hate that we get a full page even if we ask for 10 records. Can we use an ldap_control?
\Log::info("did the search run? I guess so if you got here!");
if (! $search_results) {
return redirect()->route('users.index')->with('error', trans('admin/users/message.error.ldap_could_not_search').ldap_error($ldapconn)); // FIXME this is never called in any routed context - only from the Artisan command. So this redirect will never work.
return redirect()->route('users.index')->with('error', trans('admin/users/message.error.ldap_could_not_search').ldap_error($ldapconn)); // TODO this is never called in any routed context - only from the Artisan command. So this redirect will never work.
}
$errcode = null;
$matcheddn = null;
$errmsg = null;
$referrals = null;
$controls = [];
ldap_parse_result($ldapconn, $search_results, $errcode , $matcheddn , $errmsg , $referrals, $controls);
if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
// You need to pass the cookie from the last call to the next one
$cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
\Log::debug("okay, at least one more page to go!!!");
} else {
\Log::debug("okay, we're out of pages - no cookie (or empty cookie) was passed");
$cookie = '';
}
// Empty cookie means last page
// Get results from page
$results = ldap_get_entries($ldapconn, $search_results);
if (! $results) {
return redirect()->route('users.index')->with('error', trans('admin/users/message.error.ldap_could_not_get_entries').ldap_error($ldapconn)); // FIXME this is never called in any routed context - only from the Artisan command. So this redirect will never work.
return redirect()->route('users.index')->with('error', trans('admin/users/message.error.ldap_could_not_get_entries').ldap_error($ldapconn)); // TODO this is never called in any routed context - only from the Artisan command. So this redirect will never work.
}
// Add results to result set
$global_count += $results['count'];
$result_set = array_merge($result_set, $results);
\Log::debug("Total count is: $global_count");
@ldap_control_paged_result_response($ldapconn, $search_results, $cookie);
} while ($cookie !== null && $cookie != '');
} while ($cookie !== null && $cookie != '' && ($count == -1 || $global_count < $count)); // some servers don't even have pagination, and some will give you more results than you asked for, so just see if you have enough.
// Clean up after search
$result_set['count'] = $global_count;
$result_set['count'] = $global_count; // TODO: I would've figured you could just count the array instead?
$results = $result_set;
@ldap_control_paged_result($ldapconn, 0);
return $results;
}

View file

@ -42,7 +42,7 @@ class License extends Depreciable
protected $rules = [
'name' => 'required|string|min:3|max:255',
'seats' => 'required|min:1|max:999|integer',
'seats' => 'required|min:1|integer',
'license_email' => 'email|nullable|max:120',
'license_name' => 'string|nullable|max:100',
'notes' => 'string|nullable',
@ -175,12 +175,24 @@ class License extends Depreciable
return true;
}
// Else we're adding seats.
DB::transaction(function () use ($license, $oldSeats, $newSeats) {
for ($i = $oldSeats; $i < $newSeats; $i++) {
$license->licenseSeatsRelation()->save(new LicenseSeat, ['user_id' => Auth::id()]);
}
//Create enough seats for the change.
$licenseInsert = [];
for ($i = $oldSeats; $i < $newSeats; $i++) {
$licenseInsert[] = [
'user_id' => Auth::id(),
'license_id' => $license->id,
'created_at' => now(),
'updated_at' => now()
];
}
//Chunk and use DB transactions to prevent timeouts.
collect($licenseInsert)->chunk(1000)->each(function ($chunk) {
DB::transaction(function () use ($chunk) {
LicenseSeat::insert($chunk->toArray());
});
});
// On initail create, we shouldn't log the addition of seats.
// On initial create, we shouldn't log the addition of seats.
if ($license->id) {
//Log the addition of license to the log.
$logAction = new Actionlog();

View file

@ -17,8 +17,8 @@ class Supplier extends SnipeModel
protected $rules = [
'name' => 'required|min:1|max:255|unique_undeleted',
'address' => 'max:50|nullable',
'address2' => 'max:50|nullable',
'address' => 'max:250|nullable',
'address2' => 'max:250|nullable',
'city' => 'max:255|nullable',
'state' => 'max:32|nullable',
'country' => 'max:3|nullable',

View file

@ -102,7 +102,7 @@ class AssetPresenter extends Presenter
'field' => 'employee_number',
'searchable' => false,
'sortable' => false,
'title' => trans('admin/users/table.employee_num'),
'title' => trans('general.employee_number'),
'visible' => false,
'formatter' => 'employeeNumFormatter',
], [

View file

@ -25,7 +25,7 @@ class PredefinedKitPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'title' => 'Name', // TODO: trans
'title' => trans('general.name'),
'formatter' => 'kitsLinkFormatter',
],
];
@ -84,13 +84,13 @@ class PredefinedKitPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'title' => 'Name', // TODO: trans
'title' => trans('general.name'),
'formatter' => 'modelsLinkFormatter',
], [
'field' => 'quantity',
'searchable' => false,
'sortable' => false,
'title' => 'Quantity', // TODO: trans
'title' => trans('general.quantity'),
], [
'field' => 'actions',
'searchable' => false,
@ -136,13 +136,13 @@ class PredefinedKitPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'title' => 'Name', // TODO: trans
'title' => trans('general.name'),
'formatter' => 'licensesLinkFormatter',
], [
'field' => 'quantity',
'searchable' => false,
'sortable' => false,
'title' => 'Quantity', // TODO: trans
'title' => trans('general.quantity'),
], [
'field' => 'actions',
'searchable' => false,
@ -188,13 +188,13 @@ class PredefinedKitPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'title' => 'Name', // TODO: trans
'title' => trans('general.name'),
'formatter' => 'accessoriesLinkFormatter',
], [
'field' => 'quantity',
'searchable' => false,
'sortable' => false,
'title' => 'Quantity', // TODO: trans
'title' => trans('general.quantity'),
], [
'field' => 'actions',
'searchable' => false,
@ -240,13 +240,13 @@ class PredefinedKitPresenter extends Presenter
'field' => 'name',
'searchable' => true,
'sortable' => true,
'title' => 'Name', // TODO: trans
'title' => trans('general.name'),
'formatter' => 'consumablesLinkFormatter',
], [
'field' => 'quantity',
'searchable' => false,
'sortable' => false,
'title' => 'Quantity', // TODO: trans
'title' => trans('general.quantity'),
], [
'field' => 'actions',
'searchable' => false,

View file

@ -157,7 +157,7 @@ class UserPresenter extends Presenter
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('admin/users/table.employee_num'),
'title' => trans('general.employee_number'),
'visible' => false,
],
[

View file

@ -1,39 +0,0 @@
<?php
namespace App\Providers;
use App\Services\LdapAd;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
class LdapServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->singleton(LdapAd::class, LdapAd::class);
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return [LdapAd::class];
}
}

View file

@ -1,572 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Services;
use Adldap\Adldap;
use Adldap\Models\User as AdldapUser;
use Adldap\Query\Paginator;
use Adldap\Schemas\Schema;
use App\Helpers\Helper;
use App\Models\User;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* LDAP queries.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
class LdapAd extends LdapAdConfiguration
{
/* The following is _probably_ the correct logic, but we can't use it because
some users may have been dependent upon the previous behavior, and this
could cause additional access to be available to users they don't want
to allow to log in.
$useraccountcontrol = $results[$i]['useraccountcontrol'][0];
if(
// based on MS docs at: https://support.microsoft.com/en-us/help/305144/how-to-use-useraccountcontrol-to-manipulate-user-account-properties
($useraccountcontrol & 0x200) && // is a NORMAL_ACCOUNT
!($useraccountcontrol & 0x02) && // *and* _not_ ACCOUNTDISABLE
!($useraccountcontrol & 0x10) // *and* _not_ LOCKOUT
) {
$user->activated = 1;
} else {
$user->activated = 0;
} */
const AD_USER_ACCOUNT_CONTROL_FLAGS = [
'512', // 0x200 NORMAL_ACCOUNT
'544', // 0x220 NORMAL_ACCOUNT, PASSWD_NOTREQD
'66048', // 0x10200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD
'66080', // 0x10220 NORMAL_ACCOUNT, PASSWD_NOTREQD, DONT_EXPIRE_PASSWORD
'262656', // 0x40200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED
'262688', // 0x40220 NORMAL_ACCOUNT, PASSWD_NOTREQD, SMARTCARD_REQUIRED
'328192', // 0x50200 NORMAL_ACCOUNT, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
'328224', // 0x50220 NORMAL_ACCOUNT, PASSWD_NOT_REQD, SMARTCARD_REQUIRED, DONT_EXPIRE_PASSWORD
'4260352', // 0x410200 NORMAL_ACCOUNT, DONT_EXPIRE_PASSWORD, DONT_REQ_PREAUTH
'1049088', // 0x100200 NORMAL_ACCOUNT, NOT_DELEGATED
'1114624', // 0x110200 NORMAL_ACCOUNT, NOT_DELEGATED, DONT_EXPIRE_PASSWORD
];
/**
* The LDAP results per page.
*/
const PAGE_SIZE = 500;
/**
* A base dn.
*
* @var string
*/
public $baseDn = null;
/**
* Adldap instance.
*
* @var \Adldap\Adldap
*/
protected $ldap;
/**
* Initialize LDAP from user settings
*
* @since 5.0.0
*
* @return void
*/
public function init()
{
// Already initialized
if ($this->ldap) {
return true;
}
parent::init();
if ($this->isLdapEnabled()) {
if ($this->ldapSettings['is_ad'] == 0) { //only for NON-AD setups!
$this->ldapConfig['account_prefix'] = $this->ldapSettings['ldap_auth_filter_query'];
$this->ldapConfig['account_suffix'] = ','.$this->ldapConfig['base_dn'];
} /*
To the point mentioned in ldapLogin(), we might want to add an 'else' clause here that
sets up an 'account_suffix' of '@'.$this->ldapSettings['ad_domain'] *IF* the user has
$this->ldapSettings['ad_append_domain'] enabled.
That code in ldapLogin gets simplified, in exchange for putting all the weirdness here only.
*/
$this->ldap = new Adldap();
$this->ldap->addProvider($this->ldapConfig);
return true;
}
return false;
}
public function __construct()
{
$this->init();
}
/**
* Create a user if they successfully login to the LDAP server.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @param string $username
* @param string $password
*
* @return \App\Models\User
*
* @throws Exception
*/
public function ldapLogin(string $username, string $password): User
{
if ($this->ldapSettings['ad_append_domain']) { //if you're using 'userprincipalname', don't check the ad_append_domain checkbox
$login_username = $username.'@'.$this->ldapSettings['ad_domain']; // I feel like could can be solved with the 'suffix' feature? Then this would be easier.
} else {
$login_username = $username;
}
if ($this->ldapConfig['username'] && $this->ldapConfig['password']) {
$bind_as_user = false;
} else {
$bind_as_user = true;
}
if (($this->ldap) && ($this->ldap->auth()->attempt($login_username, $password, $bind_as_user) === false)) {
throw new Exception('Unable to validate user credentials!');
}
// Should we sync the logged in user
Log::debug('Attempting to find user in LDAP directory');
$record = $this->ldap->search()->findBy($this->ldapSettings['ldap_username_field'], $username);
if ($record) {
if ($this->isLdapSync($record)) {
$this->syncUserLdapLogin($record, $password);
}
} else {
throw new Exception('Unable to find user in LDAP directory!');
}
$user = User::where('username', $username)
->whereNull('deleted_at')->where('ldap_import', '=', 1)
->where('activated', '=', '1')->first();
/* Above, I could've just done ->firstOrFail() which would've been cleaner, but it would've been miserable to
troubleshoot if it ever came up (giving a really generic and untraceable error message)
*/
if (! $user) {
throw new Exception("User is either deleted, not activated (can't log in), not from LDAP, or can't be found in database");
}
return $user;
}
/**
* Set the user information based on the LDAP settings.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @param \Adldap\Models\User $user
* @param null|Collection $defaultLocation
* @param null|Collection $mappedLocations
*
* @return null|\App\Models\User
*/
public function processUser(AdldapUser $user, ?Collection $defaultLocation = null, ?Collection $mappedLocations = null): ?User
{
// Only sync active users <- I think this actually means 'existing', not 'activated/deactivated'
if (! $user) {
return null;
}
$snipeUser = [];
$snipeUser['username'] = $user->{$this->ldapSettings['ldap_username_field']}[0] ?? '';
$snipeUser['employee_number'] = $user->{$this->ldapSettings['ldap_emp_num']}[0] ?? '';
$snipeUser['lastname'] = $user->{$this->ldapSettings['ldap_lname_field']}[0] ?? '';
$snipeUser['firstname'] = $user->{$this->ldapSettings['ldap_fname_field']}[0] ?? '';
$snipeUser['email'] = $user->{$this->ldapSettings['ldap_email']}[0] ?? '';
$snipeUser['title'] = $user->getTitle() ?? '';
$snipeUser['telephonenumber'] = $user->getTelephoneNumber() ?? '';
/*
* $locationId being 'null' means we have no per-OU location information,
* but instead of explicitly setting it to null - which would override any admin-generated
* location assignments - we just don't set it at all. For a brand new User, the 'default null'
* on the column will cover us. For an already existing user, this will not override any
* locations that were explicitly chosen by the administrators.
*
* When syncing with a particular 'default location' in mind, those should still be respected
* and it *will* override the administrators previous choices. I think this is a fair compromise.
*/
$locationId = $this->getLocationId($user, $defaultLocation, $mappedLocations);
if ($locationId !== null) {
$snipeUser['location_id'] = $locationId;
}
$activeStatus = $this->getActiveStatus($user);
if ($activeStatus !== null) {
$snipeUser['activated'] = $activeStatus;
}
return $this->setUserModel($snipeUser);
}
/**
* Set the User model information.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @param array $userInfo The user info to save to the database
*
* @return \App\Models\User
*/
public function setUserModel(array $userInfo): User
{
// If the username exists, return the user object, otherwise create a new user object
$user = User::firstOrNew([
'username' => $userInfo['username'],
]);
$user->username = $user->username ?? trim($userInfo['username']);
$user->password = $user->password ?? Helper::generateEncyrptedPassword();
$user->first_name = trim($userInfo['firstname']);
$user->last_name = trim($userInfo['lastname']);
$user->email = trim($userInfo['email']);
$user->employee_num = trim($userInfo['employee_number']);
$user->jobtitle = trim($userInfo['title']);
$user->phone = trim($userInfo['telephonenumber']);
if (array_key_exists('activated', $userInfo)) {
$user->activated = $userInfo['activated'];
} elseif (! $user->exists) { // no 'activated' flag was set or unset, *AND* this user is new - activate by default.
$user->activated = 1;
}
if (array_key_exists('location_id', $userInfo)) {
$user->location_id = $userInfo['location_id'];
}
// this is a new user
if (! isset($user->id)) {
$user->notes = 'Imported from LDAP';
}
$user->ldap_import = 1;
return $user;
}
/**
* Sync a user who has logged in by LDAP.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @param \Adldap\Models\User $record
* @param string $password
*
* @throws Exception
*/
private function syncUserLdapLogin(AdldapUser $record, string $password): void
{
$user = $this->processUser($record);
if (is_null($user->last_login)) {
$user->notes = 'Imported on first login from LDAP2';
}
if ($this->ldapSettings['ldap_pw_sync']) {
Log::debug('Syncing users password with LDAP directory.');
$user->password = bcrypt($password);
}
if (! $user->save()) {
Log::debug('Could not save user. '.$user->getErrors());
throw new Exception('Could not save user: '.$user->getErrors());
}
}
/**
* Check to see if we should sync the user with the LDAP directory.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @param \Adldap\Models\User $user
*
* @return bool
*/
private function isLdapSync(AdldapUser $user): bool
{
if (! $this->ldapSettings['ldap_active_flag']) {
return true; // always sync if you didn't define an 'active' flag
}
if ($user->{$this->ldapSettings['ldap_active_flag']} && // if your LDAP user has the aforementioned flag as an attribute *AND*
count($user->{$this->ldapSettings['ldap_active_flag']}) == 1 && // if that attribute has exactly one value *AND*
strtolower($user->{$this->ldapSettings['ldap_active_flag']}[0]) == 'false') { // that value is the string 'false' (regardless of case),
return false; // then your user is *INACTIVE* - return false
}
// otherwise, return true
return true;
}
/**
* Set the active status of the user.
* Returns 0 or 1 if the user is deactivated or activated
* or returns null if we just don't know
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @param \Adldap\Models\User $user
*
* @return int (or null)
*/
private function getActiveStatus(AdldapUser $user): ?int
{
/*
* Check to see if we are connected to an AD server
* if so, check the Active Directory User Account Control Flags
* If the admin has set their own 'active flag' - respect that instead
* (this may work to allow AD users to ignore the built-in UAC stuff that AD does)
*/
if ($user->hasAttribute($user->getSchema()->userAccountControl()) && ! $this->ldapSettings['ldap_active_flag']) {
\Log::debug('This is AD - userAccountControl is'.$user->getSchema()->userAccountControl());
$activeStatus = (in_array($user->getUserAccountControl(), self::AD_USER_ACCOUNT_CONTROL_FLAGS)) ? 1 : 0;
} else {
//\Log::debug('This looks like LDAP (or an AD where the UAC is disabled)');
// If there is no activated flag, then we can't make any determination about activated/deactivated
if (false == $this->ldapSettings['ldap_active_flag']) {
\Log::debug('ldap_active_flag is false - no ldap_active_flag is set');
return null;
}
// If there *is* an activated flag, then respect it *only* if it is actually present. If it's not there, ignore it.
if (! $user->hasAttribute($this->ldapSettings['ldap_active_flag'])) {
return null; // 'active' flag is defined, but does not exist on returned user record. So we don't know if they're active or not.
}
// if $user has the flag *AND* that flag has exactly one value -
if ($user->{$this->ldapSettings['ldap_active_flag']} && count($user->{$this->ldapSettings['ldap_active_flag']}) == 1) {
$active_flag_value = $user->{$this->ldapSettings['ldap_active_flag']}[0];
// if the value of that flag is case-insensitively the string 'false' or boolean false
if (strcasecmp($active_flag_value, 'false') == 0 || $active_flag_value === false) {
return 0; // then make them INACTIVE
} else {
return 1; // otherwise active
}
}
return 1; // fail 'open' (active) if we have the attribute and it's multivalued or empty; that's weird
}
return $activeStatus;
}
/**
* Get a default selected location, or a OU mapped location if available.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @param \Adldap\Models\User $user
* @param Collection|null $defaultLocation
* @param Collection|null $mappedLocations
*
* @return null|int
*/
private function getLocationId(AdldapUser $user, ?Collection $defaultLocation, ?Collection $mappedLocations): ?int
{
$locationId = null;
// Set the users default locations, if set
if ($defaultLocation) {
$locationId = $defaultLocation->keys()->first();
}
// Check to see if the user is in a mapped location
if ($mappedLocations) {
$location = $mappedLocations->filter(function ($value, $key) use ($user) {
//if ($user->inOu($value)) { // <----- *THIS* seems not to be working, and it seems more 'intelligent' - but it's literally just a strpos() call, and it doesn't work quite right against plain strings
$user_ou = substr($user->getDn(), -strlen($value)); // get the LAST chars of the user's DN, the count of those chars being the length of the thing we're checking against
if (strcasecmp($user_ou, $value) === 0) { // case *IN*sensitive comparision - some people say OU=blah, some say ou=blah. returns 0 when strings are identical (which is a little odd, yeah)
return $key; // WARNING: we are doing a 'filter' - not a regular for-loop. So the answer(s) get "return"ed into the $location array
}
});
if ($location->count() > 0) {
$locationId = $location->keys()->first(); // from the returned $location array from the ->filter() method above, we return the first match - there should be only one
}
}
return $locationId;
}
/**
* Get the base dn for the query.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return string
*/
private function getBaseDn(): string
{
if (! is_null($this->baseDn)) {
return $this->baseDn;
}
return $this->ldapSettings['ldap_basedn'];
}
/**
* Format the ldap filter if needed.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return null|string
*/
private function getFilter(): ?string
{
$filter = $this->ldapSettings['ldap_filter'];
if (! $filter) {
return null;
}
// Add surrounding parentheses as needed
$paren = mb_substr($filter, 0, 1, 'utf-8');
if ('(' !== $paren) {
return '('.$filter.')';
}
return $filter;
}
/**
* Get the selected fields to return
* This should help with memory on large result sets as we are not returning all fields.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return array
*/
private function getSelectedFields(): array
{
/** @var Schema $schema */
$schema = new $this->ldapConfig['schema'];
return array_values(array_filter([
$this->ldapSettings['ldap_username_field'],
$this->ldapSettings['ldap_fname_field'],
$this->ldapSettings['ldap_lname_field'],
$this->ldapSettings['ldap_email'],
$this->ldapSettings['ldap_emp_num'],
$this->ldapSettings['ldap_active_flag'],
$schema->memberOf(),
$schema->userAccountControl(),
$schema->title(),
$schema->telephone(),
]));
}
/**
* Test the bind user connection.
*
* @author Wes Hulette <jwhulette@gmail.com>
* @throws \Exception
* @since 5.0.0
*/
public function testLdapAdBindConnection(): void
{
try {
$this->ldap->search()->ous()->get()->count(); //it's saying this is null?
} catch (Exception $th) {
Log::error($th->getMessage());
throw new Exception('Unable to search LDAP directory!');
}
}
/**
* Test the user can connect to the LDAP server.
*
* @author Wes Hulette <jwhulette@gmail.com>
* @throws \Exception
* @since 5.0.0
*/
public function testLdapAdUserConnection(): void
{
try {
$this->ldap->connect();
} catch (\Exception $e) {
Log::debug('LDAP ERROR: '.$e->getMessage());
throw new Exception($e->getMessage());
}
}
/**
* Test the LDAP configuration by returning up to 10 users.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return Collection
*/
public function testUserImportSync(): Collection
{
$testUsers = collect($this->getLdapUsers()->getResults())->chunk(10)->first();
if ($testUsers) {
return $testUsers->map(function ($item) {
return (object) [
'username' => $item->{$this->ldapSettings['ldap_username_field']}[0] ?? null,
'employee_number' => $item->{$this->ldapSettings['ldap_emp_num']}[0] ?? null,
'lastname' => $item->{$this->ldapSettings['ldap_lname_field']}[0] ?? null,
'firstname' => $item->{$this->ldapSettings['ldap_fname_field']}[0] ?? null,
'email' => $item->{$this->ldapSettings['ldap_email']}[0] ?? null,
];
});
}
return collect();
}
/**
* Query the LDAP server to get the users to process and return a page set.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return \Adldap\Query\Paginator
*/
public function getLdapUsers(): Paginator
{
$search = $this->ldap->search()->users()->in($this->getBaseDn()); //this looks wrong; we should instead have a passable parameter that does this, and use this as a 'sane' default, yeah?
$filter = $this->getFilter();
if (! is_null($filter)) {
$search = $search->rawFilter($filter);
}
//I think it might be possible to potentially do our own paging here?
return $search->select($this->getSelectedFields())
->paginate(self::PAGE_SIZE);
}
}

View file

@ -1,311 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Setting;
use Exception;
use Illuminate\Support\Collection;
/**
* LDAP configuration merge for Adldap2.
*
* @see https://github.com/Adldap2/Adldap2
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
class LdapAdConfiguration
{
const LDAP_PORT = 389;
const CONNECTION_TIMEOUT = 5;
const DEFAULT_LDAP_VERSION = 3;
const LDAP_BOOLEAN_SETTINGS = [
'ldap_enabled',
'ldap_server_cert_ignore',
'ldap_tls',
'ldap_tls',
'ldap_pw_sync',
'is_ad',
'ad_append_domain',
];
/**
* Ldap Settings.
*
* @var Collection
*/
public $ldapSettings;
/**
* LDAP Config.
*
* @var array
*/
public $ldapConfig;
/**
* Initialize LDAP from user settings
*
* @since 5.0.0
*/
public function init()
{
// This try/catch is dumb, but is necessary to run initial migrations, since
// this service provider is booted even during migrations. :( - snipe
try {
$this->ldapSettings = $this->getSnipeItLdapSettings();
if ($this->isLdapEnabled()) {
$this->setSnipeItConfig();
}
} catch (\Exception $e) {
\Log::debug($e);
$this->ldapSettings = null;
}
}
/**
* Merge the default Adlap config with the SnipeIT config.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function setSnipeItConfig()
{
$this->ldapConfig = $this->setLdapConnectionConfiguration();
$this->certificateCheck();
}
/**
* Get the LDAP settings from the Settings model.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return \Illuminate\Support\Collection
*/
private function getSnipeItLdapSettings(): Collection
{
$ldapSettings = collect();
if (Setting::first()) { // during early migration steps, there may be no settings table entry to start with
$ldapSettings = Setting::getLdapSettings()
->map(function ($item, $key) {
// Trim the items
if (is_string($item)) {
$item = trim($item);
}
// Get the boolean value of the LDAP setting, makes it easier to work with them
if (in_array($key, self::LDAP_BOOLEAN_SETTINGS)) {
return boolval($item);
}
// Decrypt the admin password
if ('ldap_pword' === $key && ! empty($item)) {
try {
return decrypt($item);
} catch (Exception $e) {
throw new Exception('Your app key has changed! Could not decrypt LDAP password using your current app key, so LDAP authentication has been disabled. Login with a local account, update the LDAP password and re-enable it in Admin > Settings.');
}
}
if ($item && 'ldap_server' === $key) {
return collect(parse_url($item));
}
return $item;
});
}
return $ldapSettings;
}
/**
* Set the server certificate environment variable.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*/
private function certificateCheck(): void
{
// If we are ignoring the SSL cert we need to setup the environment variable
// before we create the connection
if ($this->ldapSettings['ldap_server_cert_ignore']) {
putenv('LDAPTLS_REQCERT=never');
}
// If the user specifies where CA Certs are, make sure to use them
if (env('LDAPTLS_CACERT')) {
putenv('LDAPTLS_CACERT='.env('LDAPTLS_CACERT'));
}
}
/**
* Set the Adlap2 connection configuration values based on SnipeIT settings.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return array
*/
private function setLdapConnectionConfiguration(): array
{
// Create the configuration array.
$ldap_settings = [
// Mandatory Configuration Options
'hosts' => $this->getServerUrlBase(),
'base_dn' => $this->ldapSettings['ldap_basedn'],
'username' => $this->ldapSettings['ldap_uname'],
'password' => $this->ldapSettings['ldap_pword'],
// Optional Configuration Options
'schema' => $this->getSchema(), // FIXME - we probably ought not to be using this, right?
'account_prefix' => '',
'account_suffix' => '',
'port' => $this->getPort(),
'follow_referrals' => false,
'use_ssl' => $this->isSsl(),
'use_tls' => $this->ldapSettings['ldap_tls'],
'version' => $this->ldapSettings['ldap_version'] ?? self::DEFAULT_LDAP_VERSION,
'timeout' => self::CONNECTION_TIMEOUT,
// Custom LDAP Options
'custom_options' => [
// See: http://php.net/ldap_set_option
// LDAP_OPT_X_TLS_REQUIRE_CERT => LDAP_OPT_X_TLS_HARD,
],
];
if($this->ldapSettings['ldap_client_tls_cert'] || $this->ldapSettings['ldap_client_tls_key']) {
$ldap_settings['custom_options'] = [
LDAP_OPT_X_TLS_CERTFILE => Setting::get_client_side_cert_path(),
LDAP_OPT_X_TLS_KEYFILE => Setting::get_client_side_key_path()
];
}
return $ldap_settings;
}
/**
* Get the schema to use for the connection.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return string
*/
private function getSchema(): string //wait, what? This is a little weird, since we have completely separate variables for this; we probably shoulnd't be using any 'schema' at all
{
$schema = \Adldap\Schemas\OpenLDAP::class;
if ($this->ldapSettings['is_ad']) {
$schema = \Adldap\Schemas\ActiveDirectory::class;
}
return $schema;
}
/**
* Get the port number from the connection url.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return int
*/
private function getPort(): int
{
$port = $this->getLdapServerData('port');
if ($port && is_int($port)) {
return $port;
}
return self::LDAP_PORT;
}
/**
* Get ldap scheme from url to determin ssl use.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return bool
*/
private function isSsl(): bool
{
$scheme = $this->getLdapServerData('scheme');
if ($scheme && 'ldaps' === strtolower($scheme)) {
return true;
}
return false;
}
/**
* Return the base url to the LDAP server.
*
* @author Wes Hulette <jwhulette@gmail.com>
*
* @since 5.0.0
*
* @return array
*/
private function getServerUrlBase(): array
{
/* if ($this->ldapSettings['is_ad']) {
return collect(explode(',', $this->ldapSettings['ad_domain']))->map(function ($item) {
return trim($item);
})->toArray();
} */ // <- this was the *original* intent of the PR for AdLdap2, but we've been moving away from having
// two separate fields - one for "ldap_host" and one for "ad_domain" - towards just using "ldap_host"
// ad_domain for us just means "append this domain to your usernames for login, if you click that checkbox"
// that's all, nothing more (I hope).
$url = $this->getLdapServerData('host');
return $url ? [$url] : [];
}
/**
* Get ldap enabled setting
*
* @author Steffen Buehl <sb@sbuehl.com>
*
* @since 5.0.0
*
* @return bool
*/
public function isLdapEnabled(): bool
{
return $this->ldapSettings && $this->ldapSettings->get('ldap_enabled');
}
/**
* Get parsed ldap server information
*
* @author Steffen Buehl <sb@sbuehl.com>
*
* @since 5.0.0
*
* @param $key
* @return mixed|null
*/
protected function getLdapServerData($key)
{
if ($this->ldapSettings) {
$ldapServer = $this->ldapSettings->get('ldap_server');
if ($ldapServer && $ldapServer instanceof Collection) {
return $ldapServer->get($key);
}
}
return null;
}
}

View file

@ -17,7 +17,6 @@
"ext-json": "*",
"ext-mbstring": "*",
"ext-pdo": "*",
"adldap2/adldap2": "^10.2",
"alek13/slack": "^2.0",
"bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-debugbar": "^3.6",

661
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -358,7 +358,6 @@ return [
* Custom service provider
*/
App\Providers\MacroServiceProvider::class,
App\Providers\LdapServiceProvider::class,
App\Providers\SamlServiceProvider::class,
],

View file

@ -39,7 +39,6 @@ class ActionlogFactory extends Factory
'item_type' => get_class($asset),
'item_id' => 1,
'user_id' => 1,
'filename' => $this->faker->word,
'action_type' => 'uploaded',
];
}
@ -49,7 +48,7 @@ class ActionlogFactory extends Factory
{
return $this->state(function () {
$target = \App\Models\User::inRandomOrder()->first();
$item = \App\Models\Asset::inRandomOrder()->RTD()->first();
$item = \App\Models\Asset::RTD()->inRandomOrder()->first();
$user_id = rand(1, 2); // keep it simple - make it one of the two superadmins
$asset = Asset::where('id', $item->id)
->update(

View file

@ -41,14 +41,13 @@ class AssetFactory extends Factory
'name' => null,
'rtd_location_id' => Location::factory()->create(),
'serial' => $this->faker->uuid,
'status_id' => StatusLabel::factory()->create()->id,
'status_id' => $this->faker->numberBetween(1,5),
'user_id' => 1,
'asset_tag' => $this->faker->unixTime('now'),
'notes' => 'Created by DB seeder',
'purchase_date' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get()),
'purchase_cost' => $this->faker->randomFloat(2, '299.99', '2999.99'),
'order_number' => $this->faker->numberBetween(1000000, 50000000),
'supplier_id' => Supplier::factory()->create(),
'requestable' => $this->faker->boolean(),
'assigned_to' => null,
'assigned_type' => null,

View file

@ -2,6 +2,7 @@
namespace Database\Factories;
use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;
/*
@ -37,18 +38,22 @@ class LicenseFactory extends Factory
*/
public function definition()
{
return [
'user_id' => 1,
'license_name' => $this->faker->name,
'name' => $this->faker->name,
'license_email' => $this->faker->safeEmail,
'serial' => $this->faker->uuid,
'notes' => 'Created by DB seeder',
'seats' => $this->faker->numberBetween(1, 10),
'purchase_date' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get()),
'order_number' => $this->faker->numberBetween(1000000, 50000000),
'expiration_date' => $this->faker->dateTimeBetween('now', '+3 years', date_default_timezone_get())->format('Y-m-d H:i:s'),
'reassignable' => $this->faker->boolean(),
'termination_date' => $this->faker->dateTimeBetween('-1 years', 'now', date_default_timezone_get())->format('Y-m-d H:i:s'),
'supplier_id' => $this->faker->numberBetween(1, 5),
'category_id' => Category::where('category_type', '=', 'license')->inRandomOrder()->first()->id
];
}

View file

@ -6,30 +6,15 @@ class CreateTempLicensesTable extends Migration
{
/**
* Run the migrations.
*
* This migration is overwritten by a later migration - 2013_11_25_recreate_licenses_table.php
*
* @return void
*/
public function up()
{
if (!Schema::hasTable('licenses')) {
Schema::create('licenses', function ($table) {
$table->increments('id');
$table->string('name');
$table->integer('model_id');
$table->text('serial');
$table->string('license_email');
$table->string('license_name');
$table->date('purchase_date')->nullable();
$table->decimal('purchase_cost', 8, 2)->nullable();
$table->string('order_number');
$table->integer('assigned_to');
$table->text('notes');
$table->integer('user_id')->nullable();
$table->timestamps();
$table->engine = 'InnoDB';
});
}
//
}
@ -41,6 +26,6 @@ class CreateTempLicensesTable extends Migration
*/
public function down()
{
Schema::dropIfExists('licenses');
// Schema::dropIfExists('licenses');
}
}

View file

@ -11,22 +11,23 @@ class ReCreateLicensesTable extends Migration
*/
public function up()
{
//
Schema::create('licenses', function ($table) {
$table->increments('id');
$table->string('name');
$table->string('serial');
$table->date('purchase_date')->nullable();
$table->decimal('purchase_cost', 8, 2)->nullable();
$table->string('order_number');
$table->integer('seats');
$table->text('notes');
$table->integer('user_id')->nullable();
$table->integer('depreciation_id');
$table->timestamps();
$table->softDeletes();
$table->engine = 'InnoDB';
});
if (!Schema::hasTable('licenses')) {
Schema::create('licenses', function ($table) {
$table->increments('id');
$table->string('name');
$table->string('serial');
$table->date('purchase_date')->nullable();
$table->decimal('purchase_cost', 8, 2)->nullable();
$table->string('order_number');
$table->integer('seats');
$table->text('notes');
$table->integer('user_id')->nullable();
$table->integer('depreciation_id');
$table->timestamps();
$table->softDeletes();
$table->engine = 'InnoDB';
});
}
}
/**

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeSupplierAddressLength extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('suppliers', function (Blueprint $table) {
//
$table->string('address', 250)->nullable()->change();
$table->string('address2', 250)->nullable()->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('suppliers', function (Blueprint $table) {
//
$table->text('address', 50)->nullable()->default(null)->change();
$table->text('address2', 50)->nullable()->default(null)->change();
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddLicenseIdIndexToLicenseSeats extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('license_seats', function (Blueprint $table) {
$table->index(['license_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('license_seats', function (Blueprint $table) {
$table->dropIndex(['license_id']);
});
}
}

View file

@ -1,72 +0,0 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\AssetModel;
use Illuminate\Support\Facades\Storage;
class AssetModelSeeder extends Seeder
{
public function run()
{
AssetModel::truncate();
// Laptops
factory(AssetModel::class, 1)->states('mbp-13-model')->create(); // 1
factory(AssetModel::class, 1)->states('mbp-air-model')->create(); // 2
factory(AssetModel::class, 1)->states('surface-model')->create(); // 3
factory(AssetModel::class, 1)->states('xps13-model')->create(); // 4
factory(AssetModel::class, 1)->states('spectre-model')->create(); // 5
factory(AssetModel::class, 1)->states('zenbook-model')->create(); // 6
factory(AssetModel::class, 1)->states('yoga-model')->create(); // 7
// Desktops
factory(AssetModel::class, 1)->states('macpro-model')->create(); // 8
factory(AssetModel::class, 1)->states('lenovo-i5-model')->create(); // 9
factory(AssetModel::class, 1)->states('optiplex-model')->create(); // 10
// Conference Phones
factory(AssetModel::class, 1)->states('polycom-model')->create(); // 11
factory(AssetModel::class, 1)->states('polycomcx-model')->create(); // 12
// Tablets
factory(AssetModel::class, 1)->states('ipad-model')->create(); // 13
factory(AssetModel::class, 1)->states('tab3-model')->create(); // 14
// Phones
factory(AssetModel::class, 1)->states('iphone11-model')->create(); // 15
factory(AssetModel::class, 1)->states('iphone12-model')->create(); // 16
// Displays
factory(AssetModel::class, 1)->states('ultrafine')->create(); // 17
factory(AssetModel::class, 1)->states('ultrasharp')->create(); // 18
$src = public_path('/img/demo/models/');
$dst = 'models'.'/';
$del_files = Storage::files($dst);
foreach($del_files as $del_file){ // iterate files
$file_to_delete = str_replace($src,'',$del_file);
\Log::debug('Deleting: '.$file_to_delete);
try {
Storage::disk('public')->delete($dst.$del_file);
} catch (\Exception $e) {
\Log::debug($e);
}
}
$add_files = glob($src."/*.*");
foreach($add_files as $add_file){
$file_to_copy = str_replace($src,'',$add_file);
\Log::debug('Copying: '.$file_to_copy);
try {
Storage::disk('public')->put($dst.$file_to_copy, file_get_contents($src.$file_to_copy));
} catch (\Exception $e) {
\Log::debug($e);
}
}
}
}

44844
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,14 +17,16 @@
"axios": "^0.20.0",
"babel-preset-latest": "^6.24.1",
"jquery": "^3.6.0",
"laravel-mix": "^6.0.12",
"laravel-mix": "^6.0.39",
"lodash": "^4.17.20",
"postcss": "^8.2.8",
"postcss": "^8.4.5",
"vue": "2.4.4",
"vue-loader": "^15.9.7",
"vue-template-compiler": "2.4.4"
},
"dependencies": {
"acorn": "^8.6.0",
"acorn-import-assertions": "^1.8.0",
"admin-lte": "^2.4.18",
"ajv": "^6.12.6",
"blueimp-file-upload": "^9.34.0",
@ -44,14 +46,16 @@
"jquery-ui": "^1.13.0",
"jquery-ui-bundle": "^1.12.1",
"jquery.iframe-transport": "^1.0.0",
"less": "^4.1.1",
"jspdf-autotable": "^3.5.23",
"less": "^4.1.2",
"less-loader": "^5.0.0",
"list.js": "^1.5.0",
"papaparse": "^4.3.3",
"select2": "4.0.13",
"sheetjs": "^2.0.0",
"tableexport.jquery.plugin": "^1.10.26",
"tableexport.jquery.plugin": "^1.21.0",
"tether": "^1.4.0",
"vue-resource": "^1.5.2"
"vue-resource": "^1.5.2",
"webpack": "^5.65.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.

Binary file not shown.

Binary file not shown.

BIN
public/js/dist/all.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -1,8 +1,8 @@
{
"/js/build/app.js": "/js/build/app.js?id=16ac5c8f218827150ce0",
"/js/build/app.js": "/js/build/app.js?id=53b8d5c2b746ea8777a7",
"/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=83e39e254b7f9035eddc",
"/css/build/overrides.css": "/css/build/overrides.css?id=b1866ec98d44c0a8ceea",
"/css/build/app.css": "/css/build/app.css?id=61d5535cb27cce41d422",
"/css/build/overrides.css": "/css/build/overrides.css?id=4fc3a0e0a16964643e70",
"/css/build/app.css": "/css/build/app.css?id=a7cd7ad6e0e053ccf443",
"/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=65ca7a34198fa16ba846",
"/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=83271cb3576583918804",
"/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=d81888449b72ecd8dd24",
@ -18,15 +18,15 @@
"/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=efda2335fa5243175850",
"/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=6a9d0ac448c28b88e5d6",
"/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=c24716a423d375902723",
"/css/dist/all.css": "/css/dist/all.css?id=3480eded2be4cd65a83e",
"/css/dist/all.css": "/css/dist/all.css?id=97ea7f220e05bd2c1d46",
"/css/blue.png": "/css/blue.png?id=e83a6c29e04fe851f212",
"/css/blue@2x.png": "/css/blue@2x.png?id=51135dd4d24f88f5de0b",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced",
"/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6e35c74c14f89f55df49",
"/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6e35c74c14f89f55df49",
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=810d7e520c3057ee500e",
"/js/build/vendor.js": "/js/build/vendor.js?id=651427cc4b45d8e68d0c",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=0f09ee116517a9573dd0",
"/js/dist/all.js": "/js/dist/all.js?id=fbc9a3fb41192f2724e8",
"/js/build/vendor.js": "/js/build/vendor.js?id=b717ba4eb48e4ce62615",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=d6794066f6af00743d5f",
"/js/dist/all.js": "/js/dist/all.js?id=e3ee59aad9b5feb67315",
"/css/dist/skins/skin-green.min.css": "/css/dist/skins/skin-green.min.css?id=efda2335fa5243175850",
"/css/dist/skins/skin-green-dark.min.css": "/css/dist/skins/skin-green-dark.min.css?id=6e35fb4cb2f1063b3047",
"/css/dist/skins/skin-black.min.css": "/css/dist/skins/skin-black.min.css?id=ec96c42439cdeb022133",

View file

@ -178,7 +178,6 @@
{id: 'phone_number', text: 'Phone Number' },
{id: 'manager_first_name', text: 'Manager First Name' },
{id: 'manager_last_name', text: 'Manager Last Name' },
{id: 'department', text: 'Department' },
{id: 'activated', text: 'Activated' },
{id: 'address', text: 'Address' },
{id: 'city', text: 'City' },

View file

@ -571,7 +571,7 @@ th.css-envelope > .th-inner,
th.css-accessory > .th-inner
{
font-size: 0px;
line-height: 4!important;
line-height: .75!important;
text-align: left;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
@ -588,41 +588,46 @@ th.css-accessory > .th-inner::before
{
display: inline-block;
font: normal normal normal 14px/1 FontAwesome;
font-size: 20px;
font-family: "Font Awesome 5 Free";
font-weight: 300;
}
th.css-padlock > .th-inner::before
{
content: "\f023";
content: "\f023"; font-family: "Font Awesome 5 Free"; font-weight: 400;
padding-right: 2px;
}
/**
Set the font-weight css property as 900 (For Solid), 400 (Regular or Brands), 300 (Light for pro icons).
**/
th.css-barcode > .th-inner::before
{
content: "\f02a";
content: "\f02a"; font-family: "Font Awesome 5 Free"; font-weight: 900;
}
th.css-license > .th-inner::before
{
content: "\f0c7";
content: "\f0c7"; font-family: "Font Awesome 5 Free"; font-weight: 400;
}
th.css-consumable > .th-inner::before
{
content: "\f043";
content: "\f043"; font-family: "Font Awesome 5 Free"; font-weight: 900;
}
th.css-envelope > .th-inner::before
{
content: "\f003";
content: "\f0e0"; font-family: "Font Awesome 5 Free"; font-weight: 400;
}
th.css-accessory > .th-inner::before
{
content: "\f11c";
content: "\f11c"; font-family: "Font Awesome 5 Free"; font-weight: 400;
}
.small-box .inner {
padding-left: 15px;
padding-right: 15px;

View file

@ -0,0 +1,12 @@
<?php
return [
'personal_api_keys' => 'Personal API Keys',
'api_key_warning' => 'When generating an API token, be sure to copy it down immediately as they
will not be visible to you again.',
'api_base_url' => 'Your API base url is located at:',
'api_base_url_endpoint' => '/&lt;endpoint&gt;',
'api_token_expiration_time' => 'API tokens are set to expire in:',
'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.',
];

View file

@ -2,4 +2,6 @@
return [
'select_company' => 'Select Company',
'about_companies' => 'About Companies',
'about_companies_description' => ' You can use companies as a simple informative field, or you can use them to restrict asset visibility and availability to users with a specific company by enabling Full Company Support in your Admin Settings.',
];

View file

@ -2,6 +2,7 @@
return [
'custom_fields' => 'Custom Fields',
'manage' => 'Manage',
'field' => 'Field',
'about_fieldsets_title' => 'About Fieldsets',
'about_fieldsets_text' => 'Fieldsets allow you to create groups of custom fields that are frequently re-used for specific asset model types.',
@ -26,7 +27,19 @@ return [
'used_by_models' => 'Used By Models',
'order' => 'Order',
'create_fieldset' => 'New Fieldset',
'create_fieldset_title' => 'Create a new fieldset',
'create_field' => 'New Custom Field',
'create_field_title' => 'Create a new custom field',
'value_encrypted' => 'The value of this field is encrypted in the database. Only admin users will be able to view the decrypted value',
'show_in_email' => 'Include the value of this field in checkout emails sent to the user? Encrypted fields cannot be included in emails.',
'help_text' => 'Help Text',
'help_text_description' => 'This is optional text that will appear below the form elements while editing an asset to provide context on the field.',
'about_custom_fields_title' => 'About Custom Fields',
'about_custom_fields_text' => 'Custom fields allow you to add arbitrary attributes to assets.',
'add_field_to_fieldset' => 'Add Field to Fieldset',
'make_optional' => 'Required - click to make optional',
'make_required' => 'Optional - click to make required',
'reorder' => 'Reorder',
'db_field' => 'DB Field',
'db_convert_warning' => 'WARNING. This field is in the custom fields table as <code> :db_column </code> but should be :expected </code>.'
];

View file

@ -9,6 +9,8 @@ return [
'depreciation_min' => 'Floor Value of Depreciation',
'number_of_months' => 'Number of Months',
'update' => 'Update Depreciation',
'depreciation_min' => 'Minimum Value after Depreciation'
'depreciation_min' => 'Minimum Value after Depreciation',
'no_depreciations_warning' => '<strong>Warning: </strong>
You do not currently have any depreciations set up.
Please set up at least one depreciation to view the depreciation report.',
];

View file

@ -10,5 +10,7 @@ return [
'group_admin' => 'Group Admin',
'allow' => 'Allow',
'deny' => 'Deny',
'permission' => 'Permission',
'grant' => 'Grant',
'no_permissions' => 'This group has no permissions.'
];

View file

@ -40,4 +40,10 @@ return [
'warranty' => 'Warranty',
'warranty_expires' => 'Warranty Expires',
'years' => 'years',
'asset_location' => 'Update Asset Location',
'asset_location_update_default_current' => 'Update default location AND actual location',
'asset_location_update_default' => 'Update only default location',
'asset_not_deployable' => 'That asset status is not deployable. This asset cannot be checked out.',
'asset_deployable' => 'That status is deployable. This asset can be checked out.',
'processing_spinner' => 'Processing...',
];

View file

@ -15,8 +15,29 @@ return [
'model_deleted' => 'This Assets model has been deleted. You must restore the model before you can restore the Asset.',
'requestable' => 'Requestable',
'requested' => 'Requested',
'not_requestable' => 'Not Requestable',
'requestable_status_warning' => 'Do not change requestable status',
'restore' => 'Restore Asset',
'pending' => 'Pending',
'undeployable' => 'Undeployable',
'view' => 'View Asset',
'csv_error' => 'You have an error in your CSV file:',
'import_text' => '
<p>
Upload a CSV that contains asset history. The assets and users MUST already exist in the system, or they will be skipped. Matching assets for history import happens against the asset tag. We will try to find a matching user based on the user\'s name you provide, and the criteria you select below. If you do not select any criteria below, it will simply try to match on the username format you configured in the Admin &gt; General Settings.
</p>
<p>Fields included in the CSV must match the headers: <strong>Asset Tag, Name, Checkout Date, Checkin Date</strong>. Any additional fields will be ignored. </p>
<p>Checkin Date: blank or future checkin dates will checkout items to associated user. Excluding the Checkin Date column will create a checkin date with todays date.</p>
',
'csv_import_match_f-l' => 'Try to match users by firstname.lastname (jane.smith) format',
'csv_import_match_initial_last' => 'Try to match users by first initial last name (jsmith) format',
'csv_import_match_first' => 'Try to match users by first name (jane) format',
'csv_import_match_email' => 'Try to match users by email as username',
'csv_import_match_username' => 'Try to match users by username',
'error_messages' => 'Error messages:',
'success_messages' => 'Success messages:',
'alert_details' => 'Please see below for details.',
'custom_export' => 'Custom Export'
];

View file

@ -19,8 +19,12 @@ return [
'serial' => 'Serial',
'status' => 'Status',
'title' => 'Asset ',
'image' => 'Device Image',
'image' => 'Device Image',
'days_without_acceptance' => 'Days Without Acceptance',
'monthly_depreciation' => 'Monthly Depreciation',
'assigned_to' => 'Assigned To',
'requesting_user' => 'Requesting User',
'requested_date' => 'Requested Date',
'changed' => 'Changed',
'icon' => 'Icon',
];

View file

@ -0,0 +1,6 @@
<?php
return [
'process' => 'Process',
];

View file

@ -0,0 +1,7 @@
<?php
return [
'select_import_file' => 'Select Import File...',
'select_file' => 'Select file'
];

View file

@ -0,0 +1,10 @@
<?php
return [
'file' => 'File',
'created' => 'Created',
'size' => 'Size',
'process' => 'Process',
'delete' => 'Delete',
];

View file

@ -13,5 +13,38 @@ return [
'none_licenses' => 'There are not enough available seats for :license to checkout. :qty are required. ',
'none_consumables' => 'There are not enough available units of :consumable to checkout. :qty are required. ',
'none_accessory' => 'There are not enough available units of :accessory to checkout. :qty are required. ',
'append_accessory' => 'Append Accessory',
'update_appended_accessory' => 'Update appended Accessory',
'append_consumable' => 'Append Consumable',
'update_appended_consumable' => 'Update appended Consumable',
'append_license' => 'Append license',
'update_appended_license' => 'Update appended license',
'append_model' => 'Append model',
'update_appended_model' => 'Update appended model',
'license_error' => 'License already attached to kit',
'license_added_success' => 'License added successfully',
'license_updated' => 'License was successfully updated',
'license_none' => 'License does not exist',
'license_detached' => 'License was successfully detached',
'consumable_added_success' => 'Consumable added successfully',
'consumable_updated' => 'Consumable was successfully updated',
'consumable_error' => 'Consumable already attached to kit',
'consumable_deleted' => 'Delete was successful',
'consumable_none' => 'Consumable does not exist',
'consumable_detached' => 'Consumable was successfully detached',
'accessory_added_success' => 'Accessory added successfully',
'accessory_updated' => 'Accessory was successfully updated',
'accessory_detached' => 'Accessory was successfully detached',
'accessory_error' => 'Accessory already attached to kit',
'accessory_deleted' => 'Delete was successful',
'accessory_none' => 'Accessory does not exist',
'checkout_success' => 'Checkout was successful',
'checkout_error' => 'Checkout error',
'kit_none' => 'Kit does not exist',
'kit_created' => 'Kit was successfully created',
'kit_updated' => 'Kit was successfully updated',
'kit_not_found' => 'Kit not found',
'kit_deleted' => 'Kit was successfully deleted',
'kit_model_updated' => 'Model was successfully updated',
'kit_model_detached' => 'Model was successfully detached',
];

View file

@ -0,0 +1,9 @@
<?php
return [
'assigned_location' => 'Assigned to :location Location',
'asset_management_system' => 'Asset Management System',
'assigned_to' => 'Assigned To:',
'manager' => 'Manager',
'date' => 'Current Date:',
];

View file

@ -20,4 +20,21 @@ return [
'parent' => 'Parent',
'currency' => 'Location Currency',
'ldap_ou' => 'LDAP Search OU',
];
'user_name' => 'User Name',
'department' => 'Department',
'location' => 'Location',
'asset_tag' => 'Assets Tag',
'asset_name' => 'Name',
'asset_category' => 'Category',
'asset_manufacturer' => 'Manufacturer',
'asset_model' => 'Model',
'asset_serial' => 'Serial',
'asset_location' => 'Location',
'asset_checked_out' => 'Checked Out',
'asset_expected_checkin' => 'Expected Checkin',
'date' => 'Date:',
'signed_by_asset_auditor' => 'Signed By (Asset Auditor):',
'signed_by_finance_auditor' => 'Signed By (Finance Auditor):',
'signed_by_location_manager' => 'Signed By (Location Manager):',
'signed_by' => 'Signed Off By:',
];

View file

@ -10,7 +10,10 @@ return [
'admin_cc_email' => 'CC Email',
'admin_cc_email_help' => 'If you would like to send a copy of checkin/checkout emails that are sent to users to an additional email account, enter it here. Otherwise leave this field blank.',
'is_ad' => 'This is an Active Directory server',
'alerts' => 'Alerts',
'alert_title' => 'Update Alert Settings',
'alert_email' => 'Send alerts to',
'alert_email_help' => 'Email addresses or distribution lists you want alerts to be sent to, comma separated',
'alerts_enabled' => 'Email Alerts Enabled',
'alert_interval' => 'Expiring Alerts Threshold (in days)',
'alert_inv_threshold' => 'Inventory Alert Threshold',
@ -25,6 +28,12 @@ return [
'auto_increment_prefix' => 'Prefix (optional)',
'auto_incrementing_help' => 'Enable auto-incrementing asset IDs first to set this',
'backups' => 'Backups',
'backups_restoring' => 'Restoring from Backup',
'backups_upload' => 'Upload Backup',
'backups_path' => 'Backups on the server are stored in <code>:path</code>',
'backups_restore_warning' => 'Use the restore button <small><span class="btn btn-xs btn-warning"><i class="text-white fas fa-retweet" aria-hidden="true"></i></span></small> to restore from a previous backup. (This does not currently work with S3 file storage or Docker.<br><br>Your <strong>entire :app_name database and any uploaded files will be completely replaced</strong> by what\'s in the backup file. ',
'backups_logged_out' => 'You will be logged out once your restore is complete.',
'backups_large' => 'Very large backups may time out on the restore attempt and may still need to be run via command line. ',
'barcode_settings' => 'Barcode Settings',
'confirm_purge' => 'Confirm Purge',
'confirm_purge_help' => 'Enter the text "DELETE" in the box below to purge your deleted records. This action cannot be undone and will PERMANENTLY delete all soft-deleted items and users. (You should make a backup first, just to be safe.)',
@ -55,12 +64,17 @@ return [
'footer_text' => 'Additional Footer Text ',
'footer_text_help' => 'This text will appear in the right-side footer. Links are allowed using <a href="https://help.github.com/articles/github-flavored-markdown/">Github flavored markdown</a>. Line breaks, headers, images, etc may result in unpredictable results.',
'general_settings' => 'General Settings',
'general_settings_keywords' => 'company support, signature, acceptance, email format, username format, images, per page, thumbnail, eula, tos, dashboard, privacy',
'general_settings_help' => 'Default EULA and more',
'generate_backup' => 'Generate Backup',
'header_color' => 'Header Color',
'info' => 'These settings let you customize certain aspects of your installation.',
'label_logo' => 'Label Logo',
'label_logo_size' => 'Square logos look best - will be displayed in the top right of each asset label. ',
'laravel' => 'Laravel Version',
'ldap' => 'LDAP',
'ldap_help' => 'LDAP/Active Directory',
'ldap_client_tls_key' => 'LDAP Client TLS Key',
'ldap_client_tls_cert' => 'LDAP Client-Side TLS Certificate',
'ldap_enabled' => 'LDAP enabled',
'ldap_integration' => 'LDAP Integration',
@ -95,9 +109,17 @@ return [
'ldap_activated_flag_help' => 'This flag is used to determine whether a user can login to Snipe-IT and does not affect the ability to check items in or out to them.',
'ldap_emp_num' => 'LDAP Employee Number',
'ldap_email' => 'LDAP Email',
'license' => 'Software License',
'ldap_test' => 'Test LDAP',
'ldap_test_sync' => 'Test LDAP Synchronization',
'license' => 'Software License',
'load_remote_text' => 'Remote Scripts',
'load_remote_help_text' => 'This Snipe-IT install can load scripts from the outside world.',
'login' => 'Login Attempts',
'login_attempt' => 'Login Attempt',
'login_ip' => 'IP Address',
'login_success' => 'Success?',
'login_user_agent' => 'User Agent',
'login_help' => 'List of attempted logins',
'login_note' => 'Login Note',
'login_note_help' => 'Optionally include a few sentences on your login screen, for example to assist people who have found a lost or stolen device. This field accepts <a href="https://help.github.com/articles/github-flavored-markdown/">Github flavored markdown</a>',
'login_remote_user_text' => 'Remote User login options',
@ -118,16 +140,28 @@ return [
'optional' => 'optional',
'per_page' => 'Results Per Page',
'php' => 'PHP Version',
'php_info' => 'PHP Info',
'php_overview' => 'PHP',
'php_overview_keywords' => 'phpinfo, system, info',
'php_overview_help' => 'PHP System info',
'php_gd_info' => 'You must install php-gd to display QR codes, see install instructions.',
'php_gd_warning' => 'PHP Image Processing and GD plugin is NOT installed.',
'pwd_secure_complexity' => 'Password Complexity',
'pwd_secure_complexity_help' => 'Select whichever password complexity rules you wish to enforce.',
'pwd_secure_complexity_disallow_same_pwd_as_user_fields' => 'Password cannot be the same as first name, last name, email, or username',
'pwd_secure_complexity_letters' => 'Require at least one letter',
'pwd_secure_complexity_numbers' => 'Require at least one number',
'pwd_secure_complexity_symbols' => 'Require at least one symbol',
'pwd_secure_complexity_case_diff' => 'Require at least one uppercase and one lowercase',
'pwd_secure_min' => 'Password minimum characters',
'pwd_secure_min_help' => 'Minimum permitted value is 8',
'pwd_secure_uncommon' => 'Prevent common passwords',
'pwd_secure_uncommon_help' => 'This will disallow users from using common passwords from the top 10,000 passwords reported in breaches.',
'qr_help' => 'Enable QR Codes first to set this',
'qr_text' => 'QR Code Text',
'saml' => 'SAML',
'saml_title' => 'Update SAML settings',
'saml_help' => 'SAML settings',
'saml_enabled' => 'SAML enabled',
'saml_integration' => 'SAML Integration',
'saml_sp_entityid' => 'Entity ID',
@ -147,6 +181,7 @@ return [
'saml_slo_help' => 'This will cause the user to be first redirected to the IdP on logout. Leave unchecked if the IdP doesn\'t correctly support SP-initiated SAML SLO.',
'saml_custom_settings' => 'SAML Custom Settings',
'saml_custom_settings_help' => 'You can specify additional settings to the onelogin/php-saml library. Use at your own risk.',
'saml_download' => 'Download Metadata',
'setting' => 'Setting',
'settings' => 'Settings',
'show_alerts_in_menu' => 'Show alerts in top menu',
@ -157,6 +192,9 @@ return [
'show_images_in_email' => 'Show images in emails',
'show_images_in_email_help' => 'Uncheck this box if your Snipe-IT installation is behind a VPN or closed network and users outside the network will not be able to load images served from this installation in their emails.',
'site_name' => 'Site Name',
'slack' => 'Slack',
'slack_title' => 'Update Slack Settings',
'slack_help' => 'Slack settings',
'slack_botname' => 'Slack Botname',
'slack_channel' => 'Slack Channel',
'slack_endpoint' => 'Slack Endpoint',
@ -173,6 +211,8 @@ return [
'update' => 'Update Settings',
'value' => 'Value',
'brand' => 'Branding',
'brand_keywords' => 'footer, logo, print, theme, skin, header, colors, color, css',
'brand_help' => 'Logo, Site Name',
'web_brand' => 'Web Branding Type',
'about_settings_title' => 'About Settings',
'about_settings_text' => 'These settings let you customize certain aspects of your installation.',
@ -184,6 +224,7 @@ return [
'privacy_policy' => 'Privacy Policy',
'privacy_policy_link_help' => 'If a url is included here, a link to your privacy policy will be included in the app footer and in any emails that the system sends out, in compliance with GDPR. ',
'purge' => 'Purge Deleted Records',
'purge_deleted' => 'Purge Deleted ',
'labels_display_bgutter' => 'Label bottom gutter',
'labels_display_sgutter' => 'Label side gutter',
'labels_fontsize' => 'Label font size',
@ -229,4 +270,51 @@ return [
'unique_serial_help_text' => 'Checking this box will enforce a uniqueness constraint on asset serials',
'zerofill_count' => 'Length of asset tags, including zerofill',
'username_format_help' => 'This setting will only be used by the import process if a username is not provided and we have to generate a username for you.',
'oauth_title' => 'OAuth API Settings',
'oauth' => 'OAuth',
'oauth_help' => 'Oauth Endpoint Settings',
'asset_tag_title' => 'Update Asset Tag Settings',
'barcode_title' => 'Update Barcode Settings',
'barcodes' => 'Barcodes',
'barcodes_help_overview' => 'Barcode &amp; QR settings',
'barcodes_help' => 'This will attempt to delete cached barcodes. This would typically only be used if your barcode settings have changed, or if your Snipe-IT URL has changed. Barcodes will be re-generated when accessed next.',
'barcodes_spinner' => 'Attempting to delete files...',
'barcode_delete_cache' => 'Delete Barcode Cache',
'branding_title' => 'Update Branding Settings',
'general_title' => 'Update General Settings',
'mail_test' => 'Send Test',
'mail_test_help' => 'This will attempt to send a test mail to :replyto.',
'filter_by_keyword' => 'Filter by setting keyword',
'security' => 'Security',
'security_title' => 'Update Security Settings',
'security_keywords' => 'password, passwords, requirements, two factor, two-factor, common passwords, remote login, logout, authentication',
'security_help' => 'Two-factor, Password Restrictions',
'groups_keywords' => 'permissions, permission groups, authorization',
'groups_help' => 'Account permission groups',
'localization' => 'Localization',
'localization_title' => 'Update Localization Settings',
'localization_keywords' => 'localization, currency, local, locale, time zone, timezone, international, internatinalization, language, languages, translation',
'localization_help' => 'Language, date display',
'notifications' => 'Notifications',
'notifications_help' => 'Email alerts, audit settings',
'asset_tags_help' => 'Incrementing and prefixes',
'labels' => 'Labels',
'labels_title' => 'Update Label Settings',
'labels_help' => 'Label sizes &amp; settings',
'purge' => 'Purge',
'purge_keywords' => 'permanently delete',
'purge_help' => 'Purge Deleted Records',
'ldap_extension_warning' => 'It does not look like the LDAP extension is installed or enabled on this server. You can still save your settings, but you will need to enable the LDAP extension for PHP before LDAP syncing or login will work.',
'ldap_ad' => 'LDAP/AD',
'employee_number' => 'Employee Number',
'create_admin_user' => 'Create a User ::',
'create_admin_success' => 'Success! Your admin user has been added!',
'create_admin_redirect' => 'Click here to go to your app login!',
'setup_migrations' => 'Database Migrations ::',
'setup_no_migrations' => 'There was nothing to migrate. Your database tables were already set up!',
'setup_successful_migrations' => 'Your database tables have been created',
'setup_migration_output' => 'Migration output:',
'setup_migration_create_user' => 'Next: Create User',
'ldap_settings_link' => 'LDAP Settings Page',
'slack_test' => 'Test <i class="fab fa-slack"></i> Integration',
];

View file

@ -11,11 +11,33 @@ return [
'file_deleted' => 'The backup file was successfully deleted. ',
'generated' => 'A new backup file was successfully created.',
'file_not_found' => 'That backup file could not be found on the server.',
'restore_warning' => 'Yes, restore it. I acknowledge that this will overwrite any existing data currently in the database. This will also log out all of your existing users (including you).',
'restore_confirm' => 'Are you sure you wish to restore your database from :filename?'
],
'purge' => [
'error' => 'An error has occurred while purging. ',
'validation_failed' => 'Your purge confirmation is incorrect. Please type the word "DELETE" in the confirmation box.',
'success' => 'Deleted records successfully purged.',
],
'mail' => [
'sending' => 'Sending Test Email...',
'success' => 'Mail sent!',
'error' => 'Mail could not be sent.',
'additional' => 'No additional error message provided. Check your mail settings and your app log.'
],
'ldap' => [
'testing' => 'Testing LDAP Connection, Binding & Query ...',
'500' => '500 Server Error. Please check your server logs for more information.',
'error' => 'Something went wrong :(',
'sync_success' => 'A sample of 10 users returned from the LDAP server based on your settings:',
'testing_authentication' => 'Testing LDAP Authentication...',
'authentication_success' => 'User authenticated against LDAP successfully!'
],
'slack' => [
'sending' => 'Sending Slack test message...',
'success_pt1' => 'Success! Check the ',
'success_pt2' => ' channel for your test message, and be sure to click SAVE below to store your settings.',
'500' => '500 Server Error.',
'error' => 'Something went wrong.',
]
];

View file

@ -0,0 +1,6 @@
<?php
return [
'created' => 'Created',
'size' => 'Size',
];

View file

@ -22,6 +22,16 @@ return [
'view_user' => 'View User :name',
'usercsv' => 'CSV file',
'two_factor_admin_optin_help' => 'Your current admin settings allow selective enforcement of two-factor authentication. ',
'two_factor_enrolled' => '2FA Device Enrolled ',
'two_factor_active' => '2FA Active ',
];
'two_factor_enrolled' => '2FA Device Enrolled',
'two_factor_active' => '2FA Active',
'user_deactivated' => 'User is de-activated',
'activation_status_warning' => 'Do not change activation status',
'group_memberships_helpblock' => 'Only superadmins may edit group memberships.',
'superadmin_permission_warning' => 'Only superadmins may grant a user superadmin access.',
'admin_permission_warning' => 'Only users with admins rights or greater may grant a user admin access.',
'remove_group_memberships' => 'Remove Group Memberships',
'warning_deletion' => 'WARNING:',
'warning_deletion_information' => 'You are about to delete the :count user(s) listed below. Super admin names are highlighted in red.',
'update_user_asssets_status' => 'Update all assets for these users to this status',
'checkin_user_properties' => 'Check in all properties associated with these users',
];

View file

@ -8,7 +8,6 @@ return [
'createuser' => 'Create User',
'deny' => 'Deny',
'email' => 'Email',
'employee_num' => 'Employee No.',
'first_name' => 'First Name',
'groupnotes' => 'Select a group to assign to the user, remember that a user takes on the permissions of the group they are assigned.',
'id' => 'Id',

View file

@ -8,11 +8,17 @@ return [
'delete' => 'Delete',
'edit' => 'Edit',
'restore' => 'Restore',
'remove' => 'Remove',
'request' => 'Request',
'submit' => 'Submit',
'upload' => 'Upload',
'select_file' => 'Select File...',
'select_files' => 'Select Files...',
'generate_labels' => '{1} Generate Label|[2,*] Generate Labels',
'send_password_link' => 'Send Password Reset Link',
'send_password_link' => 'Send Password Reset Link',
'go' => 'Go',
'bulk_actions' => 'Bulk Actions',
'add_maintenance' => 'Add Maintenance',
'append' => 'Append',
'new' => 'New',
];

View file

@ -19,7 +19,10 @@
'asset' => 'Asset',
'asset_report' => 'Asset Report',
'asset_tag' => 'Asset Tag',
'assets_available' => 'assets available',
'asset_tags' => 'Asset Tags',
'assets_available' => 'Assets available',
'accept_assets' => 'Accept Assets :name',
'accept_assets_menu' => 'Accept Assets',
'audit' => 'Audit',
'audit_report' => 'Audit Log',
'assets' => 'Assets',
@ -30,6 +33,10 @@
'bulkaudit' => 'Bulk Audit',
'bulkaudit_status' => 'Audit Status',
'bulk_checkout' => 'Bulk Checkout',
'bulk_edit' => 'Bulk Edit',
'bulk_delete' => 'Bulk Delete',
'bulk_actions' => 'Bulk Actions',
'bulk_checkin_delete' => 'Bulk Checkin &amp; Delete',
'bystatus' => 'by Status',
'cancel' => 'Cancel',
'categories' => 'Categories',
@ -62,6 +69,8 @@
'updated_at' => 'Updated at',
'currency' => '$', // this is deprecated
'current' => 'Current',
'current_password' => 'Current Password',
'customize_report' => 'Customize Report',
'custom_report' => 'Custom Asset Report',
'dashboard' => 'Dashboard',
'days' => 'days',
@ -75,13 +84,14 @@
'delete_seats' => 'Deleted Seats',
'deletion_failed' => 'Deletion failed',
'departments' => 'Departments',
'department' => 'Department',
'department' => 'Department',
'deployed' => 'Deployed',
'depreciation' => 'Depreciation',
'depreciations' => 'Depreciations',
'depreciation_report' => 'Depreciation Report',
'details' => 'Details',
'download' => 'Download',
'download_all' => 'Download All',
'depreciation' => 'Depreciation',
'download_all' => 'Download All',
'editprofile' => 'Edit Your Profile',
'eol' => 'EOL',
'email_domain' => 'Email Domain',
@ -103,10 +113,13 @@
'file_name' => 'File',
'file_type' => 'File Type',
'file_uploads' => 'File Uploads',
'file_upload' => 'File Upload',
'generate' => 'Generate',
'github_markdown' => 'This field accepts <a href="https://help.github.com/articles/github-flavored-markdown/">Github flavored markdown</a>.',
'generate_labels' => 'Generate Labels',
'github_markdown' => 'This field accepts <a href="https://help.github.com/articles/github-flavored-markdown/">Github flavored markdown</a>.',
'groups' => 'Groups',
'gravatar_email' => 'Gravatar Email Address',
'gravatar_url' => '<a href="http://gravatar.com"><small>Change your avatar at Gravatar.com</small></a>.',
'history' => 'History',
'history_for' => 'History for',
'id' => 'ID',
@ -124,6 +137,7 @@
'asset_maintenance_report' => 'Asset Maintenance Report',
'asset_maintenances' => 'Asset Maintenances',
'item' => 'Item',
'item_name' => 'Item Name',
'insufficient_permissions' => 'Insufficient permissions!',
'kits' => 'Predefined Kits',
'language' => 'Language',
@ -144,6 +158,7 @@
'logout' => 'Logout',
'lookup_by_tag' => 'Lookup by Asset Tag',
'maintenances' => 'Maintenances',
'manage_api_keys' => 'Manage API Keys',
'manufacturer' => 'Manufacturer',
'manufacturers' => 'Manufacturers',
'markdown' => 'This field allows <a href="https://help.github.com/articles/github-flavored-markdown/">Github flavored markdown</a>.',
@ -153,6 +168,7 @@
'months' => 'months',
'moreinfo' => 'More Info',
'name' => 'Name',
'new_password' => 'New Password',
'next' => 'Next',
'next_audit_date' => 'Next Audit Date',
'last_audit' => 'Last Audit',
@ -174,19 +190,26 @@
'purchase_date' => 'Purchase Date',
'qty' => 'QTY',
'quantity' => 'Quantity',
'quantity_minimum' => 'You have :count items below or almost below minimum quantity levels',
'ready_to_deploy' => 'Ready to Deploy',
'recent_activity' => 'Recent Activity',
'remaining' => 'Remaining',
'remove_company' => 'Remove Company Association',
'reports' => 'Reports',
'restored' => 'restored',
'restore' => 'Restore',
'requestable_models' => 'Requestable Models',
'requested' => 'Requested',
'requested_date' => 'Requested Date',
'requested_assets' => 'Requested Assets',
'requested_assets_menu' => 'Requested Assets',
'request_canceled' => 'Request Canceled',
'save' => 'Save',
'select' => 'Select',
'select_all' => 'Select All',
'search' => 'Search',
'select_category' => 'Select a Category',
'select_department' => 'Select a Department',
'select_department' => 'Select a Department',
'select_depreciation' => 'Select a Depreciation Type',
'select_location' => 'Select a Location',
'select_manufacturer' => 'Select a Manufacturer',
@ -203,6 +226,7 @@
'sign_in' => 'Sign in',
'signature' => 'Signature',
'skin' => 'Skin',
'slack_msg_note' => 'A slack message will be sent',
'slack_test_msg' => 'Oh hai! Looks like your Slack integration with Snipe-IT is working!',
'some_features_disabled' => 'DEMO MODE: Some features are disabled for this installation.',
'site_name' => 'Site Name',
@ -214,6 +238,7 @@
'sure_to_delete' => 'Are you sure you wish to delete',
'submit' => 'Submit',
'target' => 'Target',
'toggle_navigation' => 'Toogle Navigation',
'time_and_date_display' => 'Time and Date Display',
'total_assets' => 'total assets',
'total_licenses' => 'total licenses',
@ -233,6 +258,7 @@
'users' => 'Users',
'viewall' => 'View All',
'viewassets' => 'View Assigned Assets',
'viewassetsfor' => 'View Assets for :name',
'website' => 'Website',
'welcome' => 'Welcome, :name',
'years' => 'years',
@ -246,10 +272,78 @@
'accept' => 'Accept :asset',
'i_accept' => 'I accept',
'i_decline' => 'I decline',
'accept_decline' => 'Accept/Decline',
'sign_tos' => 'Sign below to indicate that you agree to the terms of service:',
'clear_signature' => 'Clear Signature',
'show_help' => 'Show help',
'hide_help' => 'Hide help',
'view_all' => 'view all',
'hide_deleted' => 'Hide Deleted',
];
'email' => 'Email',
'do_not_change' => 'Do Not Change',
'bug_report' => 'Report a Bug',
'user_manual' => 'User\'s Manual',
'setup_step_1' => 'Step 1',
'setup_step_2' => 'Step 2',
'setup_step_3' => 'Step 3',
'setup_step_4' => 'Step 4',
'setup_config_check' => 'Configuration Check',
'setup_create_database' => 'Create Database Tables',
'setup_create_admin' => 'Create Admin User',
'setup_done' => 'Finished!',
'bulk_edit_about_to' => 'You are about to edit the following: ',
'checked_out' => 'Checked Out',
'checked_out_to' => 'Checked out to',
'fields' => 'Fields',
'last_checkout' => 'Last Checkout',
'due_to_checkin' => 'The following :count items are due to be checked in soon:',
'expected_checkin' => 'Expected Checkin',
'reminder_checked_out_items' => 'This is a reminder of the items currently checked out to you. If you feel this list is inaccurate (something is missing, or something appears here that you believe you never received), please email :reply_to_name at :reply_to_address.',
'changed' => 'Changed',
'to' => 'To',
'report_fields_info' => '<p>Select the fields you would like to include in your custom report, and click Generate. The file (custom-asset-report-YYYY-mm-dd.csv) will download automatically, and you can open it in Excel.</p>
<p>If you would like to export only certain assets, use the options below to fine-tune your results.</p>',
'range' => 'Range',
'bom_remark' => 'Add a BOM (byte-order mark) to this CSV',
'improvements' => 'Improvements',
'information' => 'Information',
'permissions' => 'Permissions',
'managed_ldap' => '(Managed via LDAP)',
'export' => 'Export',
'ldap_sync' => 'LDAP Sync',
'ldap_user_sync' => 'LDAP User Sync',
'synchronize' => 'Synchronize',
'sync_results' => 'Synchronization Results',
'license_serial' => 'Serial/Product Key',
'invalid_category' => 'Invalid category',
'dashboard_info' => 'This is your dashboard. There are many like it, but this one is yours.',
'60_percent_warning' => '60% Complete (warning)',
'dashboard_empty' => 'It looks like you haven not added anything yet, so we do not have anything awesome to display. Get started by adding some assets, accessories, consumables, or licenses now!',
'new_asset' => 'New Asset',
'new_license' => 'New License',
'new_accessory' => 'New Accessory',
'new_consumable' => 'New Consumable',
'collapse' => 'Collapse',
'assigned' => 'Assigned',
'asset_count' => 'Asset Count',
'accessories_count' => 'Accessories Count',
'consumables_count' => 'Consumables Count',
'components_count' => 'Components Count',
'licenses_count' => 'Licenses Count',
'notification_error' => 'Error:',
'notification_error_hint' => 'Please check the form below for errors',
'notification_success' => 'Success:',
'notification_warning' => 'Warning:',
'notification_info' => 'Info:',
'asset_information' => 'Asset Information',
'model_name' => 'Model Name:',
'asset_name' => 'Asset Name:',
'consumable_information' => 'Consumable Information:',
'consumable_name' => 'Consumable Name:',
'accessory_information' => 'Accessory Information:',
'accessory_name' => 'Accessory Name:',
'clone_item' => 'Clone Item',
'checkout_tooltip' => 'Check this item out',
'checkin_tooltip' => 'Check this item in',
'checkout_user_tooltip' => 'Check this item out to a user',
];

View file

@ -0,0 +1,6 @@
<?php
return [
'alt_uploaded_image_thumbnail' => 'Uploaded image thumbnail',
'placeholder_kit' => 'Select a kit',
];

View file

@ -6,5 +6,4 @@ return [
'action' => 'Action',
'by' => 'By',
'item' => 'Item',
];

View file

@ -2,7 +2,7 @@
{{-- Page title --}}
@section('title')
{{trans('general.accept', ['asset' => $item->present()->name()])}}
{{ trans('general.accept', ['asset' => $item->present()->name()]) }}
@parent
@stop

View file

@ -2,7 +2,7 @@
{{-- Page title --}}
@section('title')
Accept {{ $acceptance->checkoutable->present()->name() }}
{{ trans('general.accept', array('asset' => $acceptance->checkoutable->present()->name())) }}
@parent
@stop
@ -47,14 +47,14 @@
<div class="radio">
<label>
<input type="radio" name="asset_acceptance" id="accepted" value="accepted">
I accept
{{ trans('general.i_accept') }}
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="asset_acceptance" id="declined" value="declined">
I decline
{{ trans('general.i_decline') }}
</label>
</div>
@ -69,7 +69,7 @@
@if ($snipeSettings->require_accept_signature=='1')
<div class="col-md-12 col-sm-12 text-center" style="padding-top: 20px">
<h3>Sign below to indicate that you agree to the terms of service:</h3>
<h3>{{ trans('general.sign_tos') }}</h3>
<div id="signature-pad" class="m-signature-pad">
<div class="m-signature-pad--body col-md-12 col-sm-12 col-lg-12 col-xs-12">
@ -77,7 +77,7 @@
<input type="hidden" name="signature_output" id="signature_output">
</div>
<div class="col-md-12 col-sm-12 col-lg-12 col-xs-12 text-center">
<button type="button" class="btn btn-sm btn-default clear" data-action="clear" id="clear_button">Clear Signature</button>
<button type="button" class="btn btn-sm btn-default clear" data-action="clear" id="clear_button">{{ trans('general.clear_signature') }}</button>
</div>
</div>
</div> <!-- .col-md-12.text-center-->

View file

@ -2,7 +2,7 @@
{{-- Page title --}}
@section('title')
Accept assets {{ $user->present()->fullName() }}
{{ trans('general.accept_assets', array('name' => $user->present()->fullName())) }}
@parent
@stop
@ -34,15 +34,15 @@ Accept assets {{ $user->present()->fullName() }}
}'>
<thead>
<tr>
<th>Name</th>
<th>Actions</th>
<th>{{ trans('general.name')}}</th>
<th>{{ trans('table.actions')}}</th>
</tr>
</thead>
<tbody>
@foreach ($acceptances as $acceptance)
<tr>
<td>{{ ($acceptance->checkoutable) ? $acceptance->checkoutable->present()->name : '' }}</td>
<td><a href="{{ route('account.accept.item', $acceptance) }}" class="btn btn-default btn-sm">Accept/Decline</a></td>
<td><a href="{{ route('account.accept.item', $acceptance) }}" class="btn btn-default btn-sm">{{ trans('general.accept_decline') }}</a></td>
</tr>
@endforeach
</tbody>

View file

@ -2,7 +2,7 @@
{{-- Page title --}}
@section('title')
Personal API Keys
{{ trans('account/general.personal_api_keys') }}
@parent
@stop
@ -24,18 +24,17 @@
</div>
<div class="col-md-4">
<div class="alert alert-warning"><i class="fas fa-exclamation-triangle faa-pulse animated"></i>
When generating an API token, be sure to copy it down immediately as they
will not be visible to you again. </div>
{{ trans('account/general.api_key_warning') }}
</div>
<p>Your API base url is located at:<br>
<code>{{ url('/api/v1') }}/&lt;endpoint&gt;</code></p>
<p>{{ trans('account/general.api_base_url') }}<br>
<code>{{ url('/api/v1') }}{{!! trans('account/general.api_base_url_endpoint') !!}}</code></p>
<p>API tokens are set to expire in:
<strong>{{ config('passport.expiration_years') }} years</strong>.</p>
<p>{{ trans('account/general.api_token_expiration_time') }}
<strong>{{ config('passport.expiration_years') }} {{ trans('general.years') }} </strong>.</p>
<p>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.</p>
<p>{{!! trans('account/general.api_reference') !!}}</p>
</div>
</div>

View file

@ -20,7 +20,7 @@
<!-- Old Password -->
<div class="form-group {{ $errors->has('current_password') ? ' has-error' : '' }}">
<label for="current_password" class="col-md-3 control-label">Current Password
<label for="current_password" class="col-md-3 control-label"> {{ trans('general.current_password') }} </label>
</label>
<div class="col-md-5 required">
<input class="form-control" type="password" name="current_password" id="current_password" {{ (config('app.lock_passwords') ? ' disabled' : '') }}>
@ -32,7 +32,7 @@
</div>
<div class="form-group {{ $errors->has('password') ? ' has-error' : '' }}">
<label for="password" class="col-md-3 control-label">New Password</label>
<label for="password" class="col-md-3 control-label">{{ trans('general.new_password') }}</label>
<div class="col-md-5 required">
<input class="form-control" type="password" name="password" id="password" {{ (config('app.lock_passwords') ? ' disabled' : '') }}>
{!! $errors->first('password', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
@ -44,7 +44,7 @@
<div class="form-group {{ $errors->has('password_confirmation') ? ' has-error' : '' }}">
<label for="password_confirmation" class="col-md-3 control-label">New Password</label>
<label for="password_confirmation" class="col-md-3 control-label">{{ trans('general.new_password') }}</label>
<div class="col-md-5 required">
<input class="form-control" type="password" name="password_confirmation" id="password_confirmation" {{ (config('app.lock_passwords') ? ' disabled' : '') }} aria-label="password_confirmation">
{!! $errors->first('password_confirmation', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}

View file

@ -97,7 +97,7 @@
{!! $errors->first('gravatar', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
<p>
<img src="//secure.gravatar.com/avatar/{{ md5(strtolower(trim($user->gravatar))) }}" width="30" height="30" alt="{{ $user->present()->fullName() }} avatar image">
<a href="http://gravatar.com"><small>Change your avatar at Gravatar.com</small></a>.
{{!! trans('general.gravatar_url') !!}}
</p>
</div>
</div>

View file

@ -72,7 +72,7 @@
<div class="col-md-12">
@if ($models->count() > 0)
<h2>Requestable Models</h4>
<h2>{{ trans('general.requestable_models') }}</h4>
<table
name="requested-assets"
data-toolbar="#toolbar"

View file

@ -2,7 +2,7 @@
{{-- Page title --}}
@section('title')
Requested Assets
{{ trans('general.requested_assets')}}
@stop
{{-- Account page content --}}
@ -32,13 +32,13 @@
}'>
<thead>
<tr>
<th class="col-md-1" data-field="image" data-formatter="imageFormatter">Image</th>
<th class="col-md-2" data-field="name">Item Name</th>
<th class="col-md-2" data-field="type">Type</th>
<th class="col-md-1" data-field="image" data-formatter="imageFormatter">{{ trans('general.image') }}</th>
<th class="col-md-2" data-field="name">{{ trans('general.item_name') }}</th>
<th class="col-md-2" data-field="type">{{ trans('general.type') }}</th>
<th class="col-md-2" data-field="qty">{{ trans('general.qty') }}</th>
<th class="col-md-2" data-field="location">{{ trans('admin/hardware/table.location') }}</th>
<th class="col-md-2" data-field="expected_checkin" data-formatter="dateDisplayFormatter"> {{ trans('admin/hardware/form.expected_checkin') }}</th>
<th class="col-md-2" data-field="request_date" data-formatter="dateDisplayFormatter">Requested Date</th>
<th class="col-md-2" data-field="request_date" data-formatter="dateDisplayFormatter"> {{ trans('general.requested_date') }}</th>
</tr>
</thead>
</table>

View file

@ -2,7 +2,7 @@
{{-- Page title --}}
@section('title')
View Assets for {{ $user->present()->fullName() }}
{{ trans('general.viewassetsfor', array('name' => $user->present()->fullName())) }}
@parent
@stop
@ -294,7 +294,7 @@ View Assets for {{ $user->present()->fullName() }}
@if ($user->id)
<div class="box-header with-border">
<div class="box-heading">
<h2 class="box-title"> History</h2>
<h2 class="box-title"> {{ trans('general.history') }}</h2>
</div>
</div><!-- /.box-header -->
@endif

View file

@ -44,10 +44,8 @@
</div>
<!-- side address column -->
<div class="col-md-3">
<h2>About Companies</h2>
<p>
You can use companies as a simple informative field, or you can use them to restrict asset visibility and availability to users with a specific company by enabling Full Company Support in your Admin Settings.
</p>
<h2>{{ trans('admin/companies/general.about_companies') }}</h2>
<p>{{ trans('admin/companies/general.about_companies_description') }}</p>
</div>
@stop

View file

@ -96,11 +96,11 @@
<!-- Help Text -->
<div class="form-group {{ $errors->has('help_text') ? ' has-error' : '' }}">
<label for="help_text" class="col-md-4 control-label">
Help Text
{{ trans('admin/custom_fields/general.help_text') }}
</label>
<div class="col-md-6">
{{ Form::text('help_text', old('help_text', $field->help_text), array('class' => 'form-control', 'aria-label'=>'help_text')) }}
<p class="help-block">This is optional text that will appear below the form elements while editing an asset to provide context on the field.</p>
<p class="help-block">{{ trans('admin/custom_fields/general.help_text_description') }}</p>
{!! $errors->first('help_text', '<span class="alert-msg" aria-hidden="true"><i class="fas fa-times" aria-hidden="true"></i> :message</span>') !!}
</div>
</div>
@ -147,8 +147,8 @@
</div> <!--/.col-md-9-->
<div class="col-md-3">
<h2>About Custom Fields</h2>
<p>Custom fields allow you to add arbitrary attributes to assets.</p>
<h2>{{ trans('admin/custom_fields/general.about_custom_fields_title') }}</h2>
<p>{{ trans('admin/custom_fields/general.about_custom_fields_text') }}</p>
</div>
</div>

View file

@ -42,8 +42,8 @@
{{ Form::close() }}
</div>
<div class="col-md-3">
<h2>About Fieldsets</h4>
<p>Fieldsets work like containers of the custom fields you've created. This allows you to group commonly used custom attributes together for easy associations. </p>
<h2>{{ trans('admin/custom_fields/general.about_fieldsets_title') }}</h4>
<p>{{ trans('admin/custom_fields/general.about_fieldsets_text') }}</p>
</div>
</div>
@stop

View file

@ -27,7 +27,7 @@
<tr>
{{-- Hide the sorting handle if we can't update the fieldset --}}
@can('update', $custom_fieldset)
<th class="col-md-1"><span class="sr-only">Reorder</span></th>
<th class="col-md-1"><span class="sr-only">{{ trans('admin/custom_fields/general.reorder') }}</span></th>
@endcan
<th class="col-md-1">{{ trans('admin/custom_fields/general.order') }}</th>
<th class="col-md-3">{{ trans('admin/custom_fields/general.field_name') }}</th>
@ -35,7 +35,7 @@
<th class="col-md-2">{{ trans('admin/custom_fields/general.field_element') }}</th>
<th class="col-md-1">{{ trans('admin/custom_fields/general.encrypted') }}</th>
<th class="col-md-1">{{ trans('admin/custom_fields/general.required') }}</th>
<th class="col-md-1"><span class="sr-only">Remove</span></th>
<th class="col-md-1"><span class="sr-only">{{ trans('button.remove') }}</span></th>
</tr>
</thead>
<tbody>
@ -57,7 +57,7 @@
<td>{{$field->element}}</td>
<td>{{ $field->field_encrypted=='1' ? trans('general.yes') : trans('general.no') }}</td>
<td>
@if ($field->pivot->required)
<form method="post" action="{{ route('fields.optional', [$custom_fieldset->id, $field->id]) }}">
@csrf
@ -71,13 +71,13 @@
<button type="submit" class="btn btn-link"><i class="fa fa-times text-danger" aria-hidden="true"></i></button>
</form>
@endif
</td>
<td>
@can('update', $custom_fieldset)
<form method="post" action="{{ route('fields.disassociate', [$field, $custom_fieldset->id]) }}">
@csrf
<button type="submit" class="btn btn-sm btn-danger">Remove</button>
<button type="submit" class="btn btn-sm btn-danger">{{ trans('button.remove') }}</button>
</form>
@endcan
</td>
@ -96,7 +96,7 @@
<div class="form-group col-md-4">
<label for="field_id" class="sr-only">
Add Field to Fieldset
{{ trans('admin/custom-field/general.add_field_to_fieldset')}}
</label>
{{ Form::select("field_id",$custom_fields_list,"",['aria-label'=>'field_id', 'class'=>'select2']) }}
@ -113,7 +113,7 @@
<div class="form-group col-md-2">
{{ Form::text('order', $maxid, array('class' => 'form-control col-sm-1 col-md-1', 'style'=> 'width: 80px; padding-;right: 10px;', 'aria-label'=>'order', 'maxlength'=>'3', 'size'=>'3')) }}
<label for="order"> Order </label>
<label for="order">{{ trans('admin/custom_fields/general.order') }}</label>
</div>
<div class="form-group col-md-3">

View file

@ -6,7 +6,7 @@
{{-- Page title --}}
@section('title')
Manage {{ trans('admin/custom_fields/general.custom_fields') }}
{{ trans('admin/custom_fields/general.manage') }} {{ trans('admin/custom_fields/general.custom_fields') }}
@parent
@stop
@ -21,7 +21,7 @@
<h2 class="box-title">{{ trans('admin/custom_fields/general.fieldsets') }}</h2>
<div class="box-tools pull-right">
@can('create', \App\Models\CustomFieldset::class)
<a href="{{ route('fieldsets.create') }}" class="btn btn-sm btn-primary" data-toggle="tooltip" title="Create a new fieldset">{{ trans('admin/custom_fields/general.create_fieldset') }}</a>
<a href="{{ route('fieldsets.create') }}" class="btn btn-sm btn-primary" data-toggle="tooltip" title="{{ trans('admin/custom_fields/general.create_fieldset_title') }}">{{ trans('admin/custom_fields/general.create_fieldset') }}</a>
@endcan
</div>
</div><!-- /.box-header -->
@ -48,7 +48,7 @@
<th>{{ trans('general.name') }}</th>
<th>{{ trans('admin/custom_fields/general.qty_fields') }}</th>
<th>{{ trans('admin/custom_fields/general.used_by_models') }}</th>
<th>Actions</th>
<th>{{ trans('table.actions') }}</th>
</tr>
</thead>
@ -100,7 +100,7 @@
<h2 class="box-title">{{ trans('admin/custom_fields/general.custom_fields') }}</h2>
<div class="box-tools pull-right">
@can('create', \App\Models\CustomField::class)
<a href="{{ route('fields.create') }}" class="btn btn-sm btn-primary" data-toggle="tooltip" title="Create a new custom field">{{ trans('admin/custom_fields/general.create_field') }}</a>
<a href="{{ route('fields.create') }}" class="btn btn-sm btn-primary" data-toggle="tooltip" title="{{ trans('admin/custom_fields/general.create_field_title') }}">{{ trans('admin/custom_fields/general.create_field') }}</a>
@endcan
</div>
@ -127,13 +127,13 @@
<thead>
<tr>
<th data-searchable="true">{{ trans('general.name') }}</th>
<th data-searchable="true">Help Text</th>
<th data-searchable="true">Email</th>
<th data-visible="false">DB Field</th>
<th data-searchable="true">{{ trans('admin/custom_fields/general.help_text')}}</th>
<th data-searchable="true">{{ trans('general.email') }}</th>
<th data-visible="false">{{ trans('admin/custom_fields/general.db_field') }}</th>
<th data-searchable="true">{{ trans('admin/custom_fields/general.field_format') }}</th>
<th data-searchable="true">{{ trans('admin/custom_fields/general.field_element_short') }}</th>
<th data-searchable="true">{{ trans('admin/custom_fields/general.fieldsets') }}</th>
<th>Actions</th>
<th>{{ trans('button.actions') }}</th>
</tr>
</thead>
<tbody>
@ -145,7 +145,7 @@
<td>
<code>{{ $field->convertUnicodeDbSlug() }}</code>
@if ($field->convertUnicodeDbSlug()!=$field->db_column)
<br><i class="fas fa-exclamation-triangle text-danger"></i>WARNING. This field is in the custom fields table as <code>{{ $field->db_column }}</code> but should be <code>{{ $field->convertUnicodeDbSlug() }}</code>.
<br><i class="fas fa-exclamation-triangle text-danger"></i>{{!! trans('admin/custom_fields/general.db_convert_warning', array('db_column' => $field->db_column, 'expected' => $field->convertUnicodeDbSlug())) !!}}
@endif
</td>
<td>{{ $field->format }}</td>
@ -160,19 +160,19 @@
@can('update', $field)
<a href="{{ route('fields.edit', $field->id) }}" class="btn btn-warning btn-sm">
<i class="fas fa-pencil-alt" aria-hidden="true"></i>
<span class="sr-only">Edit</span>
<span class="sr-only">{{ trans('button.edit') }}</span>
</a>
@endcan
@endcan
@can('delete', $field)
{{ Form::open(array('route' => array('fields.destroy', $field->id), 'method' => 'delete', 'style' => 'display:inline-block')) }}
@if($field->fieldset->count()>0)
<button type="submit" class="btn btn-danger btn-sm disabled" disabled>
<i class="fas fa-trash" aria-hidden="true"></i>
<span class="sr-only">Delete</span></button>
<span class="sr-only">{{ trans('button.delete') }}</span></button>
@else
<button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-trash" aria-hidden="true"></i>
<span class="sr-only">Delete</span>
<span class="sr-only">{{ trans('button.delete') }}</span>
</button>
@endif
{{ Form::close() }}

View file

@ -1,5 +1,4 @@
@extends('layouts/default')
{{-- Page title --}}
@section('title')
{{ trans('general.dashboard') }}
@ -147,7 +146,7 @@
<div class="col-md-12">
<div class="box">
<div class="box-header with-border">
<h2 class="box-title">This is your dashboard. There are many like it, but this one is yours.</h2>
<h2 class="box-title">{{ trans('general.dashboard_info') }}</h2>
</div>
<!-- /.box-header -->
<div class="box-body">
@ -156,34 +155,34 @@
<div class="progress">
<div class="progress-bar progress-bar-yellow" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 60%">
<span class="sr-only">60% Complete (warning)</span>
<span class="sr-only">{{ trans('general.60_percent_warning') }}</span>
</div>
</div>
<p><strong>It looks like you haven't added anything yet, so we don't have anything awesome to display. Get started by adding some assets, accessories, consumables, or licenses now!</strong></p>
<p><strong>{{ trans('general.dashboard_empty') }}</strong></p>
</div>
</div>
<div class="row">
<div class="col-md-3">
@can('create', \App\Models\Asset::class)
<a class="btn bg-teal" style="width: 100%" href="{{ route('hardware.create') }}">New Asset</a>
<a class="btn bg-teal" style="width: 100%" href="{{ route('hardware.create') }}">{{ trans('general.new_asset') }}</a>
@endcan
</div>
<div class="col-md-3">
@can('create', \App\Models\License::class)
<a class="btn bg-maroon" style="width: 100%" href="{{ route('licenses.create') }}">New License</a>
<a class="btn bg-maroon" style="width: 100%" href="{{ route('licenses.create') }}">{{ trans('general.new_license') }}</a>
@endcan
</div>
<div class="col-md-3">
@can('create', \App\Models\Accessory::class)
<a class="btn bg-orange" style="width: 100%" href="{{ route('accessories.create') }}">New Accessory</a>
<a class="btn bg-orange" style="width: 100%" href="{{ route('accessories.create') }}">{{ trans('general.new_accessory') }}</a>
@endcan
</div>
<div class="col-md-3">
@can('create', \App\Models\Consumable::class)
<a class="btn bg-purple" style="width: 100%" href="{{ route('consumables.create') }}">New Consumable</a>
<a class="btn bg-purple" style="width: 100%" href="{{ route('consumables.create') }}">{{ trans('general.new_consumable') }}</a>
@endcan
</div>
</div>
@ -203,7 +202,7 @@
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse" aria-hidden="true">
<i class="fas fa-minus" aria-hidden="true"></i>
<span class="sr-only">Collapse</span>
<span class="sr-only">{{ trans('general.collapse') }}</span>
</button>
</div>
</div><!-- /.box-header -->
@ -225,7 +224,7 @@
data-url="{{ route('api.activity.index', ['limit' => 25]) }}">
<thead>
<tr>
<th data-field="icon" data-visible="true" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter"><span class="sr-only">Icon</span></th>
<th data-field="icon" data-visible="true" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter"><span class="sr-only">{{ trans('admin/hardware/table.icon') }}</span></th>
<th class="col-sm-3" data-visible="true" data-field="created_at" data-formatter="dateDisplayFormatter">{{ trans('general.date') }}</th>
<th class="col-sm-2" data-visible="true" data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.admin') }}</th>
<th class="col-sm-2" data-visible="true" data-field="action_type">{{ trans('general.action') }}</th>
@ -249,11 +248,11 @@
<div class="col-md-4">
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title">{{ trans('general.assets') }} by Status</h2>
<h2 class="box-title">{{ trans('general.assets') }} {{ trans('general.bystatus') }}</h2>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse" aria-hidden="true">
<i class="fas fa-minus" aria-hidden="true"></i>
<span class="sr-only">Collapse</span>
<span class="sr-only">{{ trans('general.collapse') }}</span>
</button>
</div>
</div>
@ -276,11 +275,11 @@
<!-- Categories -->
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title">Asset {{ trans('general.locations') }}</h2>
<h2 class="box-title">{{ trans('general.asset') }} {{ trans('general.locations') }}</h2>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse">
<i class="fas fa-minus" aria-hidden="true"></i>
<span class="sr-only">Collapse</span>
<span class="sr-only">{{ trans('general.collapse') }}</span>
</button>
</div>
</div>
@ -306,15 +305,15 @@
<th class="col-sm-1" data-visible="true" data-field="assets_count" data-sortable="true">
<i class="fas fa-barcode" aria-hidden="true"></i>
<span class="sr-only">Asset Count</span>
<span class="sr-only">{{ trans('general.asset_count') }}</span>
</th>
<th class="col-sm-1" data-visible="true" data-field="assigned_assets_count" data-sortable="true">
Assigned
{{ trans('general.assigned') }}
</th>
<th class="col-sm-1" data-visible="true" data-field="users_count" data-sortable="true">
<i class="fas fa-users" aria-hidden="true"></i>
<span class="sr-only">People</span>
<span class="sr-only">{{ trans('general.people') }}</span>
</th>
@ -336,11 +335,11 @@
<!-- Categories -->
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title">Asset {{ trans('general.categories') }}</h2>
<h2 class="box-title">{{ trans('general.asset') }} {{ trans('general.categories') }}</h2>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse">
<i class="fas fa-minus" aria-hidden="true"></i>
<span class="sr-only">Collapse</span>
<span class="sr-only">{{ trans('general.collapse') }}</span>
</button>
</div>
</div>
@ -368,23 +367,23 @@
</th>
<th class="col-sm-1" data-visible="true" data-field="assets_count" data-sortable="true">
<i class="fas fa-barcode" aria-hidden="true"></i>
<span class="sr-only">Asset Count</span>
<span class="sr-only">{{ trans('general.asset_count') }}</span>
</th>
<th class="col-sm-1" data-visible="true" data-field="accessories_count" data-sortable="true">
<i class="far fa-keyboard" aria-hidden="true"></i>
<span class="sr-only">Accessories Count</span>
<span class="sr-only">{{ trans('general.accessories_count') }}</span>
</th>
<th class="col-sm-1" data-visible="true" data-field="consumables_count" data-sortable="true">
<i class="fas fa-tint" aria-hidden="true"></i>
<span class="sr-only">Consumables Count</span>
<span class="sr-only">{{ trans('general.consumables_count') }}</span>
</th>
<th class="col-sm-1" data-visible="true" data-field="components_count" data-sortable="true">
<i class="far fa-hdd" aria-hidden="true"></i>
<span class="sr-only">Components Count</span>
<span class="sr-only">{{ trans('general.components_count') }}</span>
</th>
<th class="col-sm-1" data-visible="true" data-field="licenses_count" data-sortable="true">
<i class="far fa-save" aria-hidden="true"></i>
<span class="sr-only">Licenses Count</span>
<span class="sr-only">{{ trans('general.licenses_count') }}</span>
</th>
</tr>
</thead>

View file

@ -2,7 +2,7 @@
{{-- Page title --}}
@section('title')
Depreciations
{{ trans('general.depreciations')}}
@parent
@stop

View file

@ -111,12 +111,12 @@
) }}
<div class="col-md-12">
<div id="toolbar">
<label for="bulk_actions" class="sr-only">Bulk Actions</label>
<label for="bulk_actions" class="sr-only">{{ trans('general.bulk_actions') }}</label>
<select name="bulk_actions" class="form-control select2" aria-label="bulk_actions" style="width: 300px;">
<option value="edit">Bulk Edit</option>
<option value="delete">Bulk Delete</option>
<option value="edit">{{ trans('general.bulk_edit') }}</option>
<option value="delete">{{ trans('general.bulk_delete') }}</option>
</select>
<button class="btn btn-primary" id="bulkEdit" disabled>Go</button>
<button class="btn btn-primary" id="bulkEdit" disabled>{{ trans('button.go') }}</button>
</div>
<div class="table-responsive">

View file

@ -65,9 +65,9 @@
<table class="table table-striped permissions">
<thead>
<tr class="permissions-row">
<th class="col-md-5">Permission</th>
<th class="col-md-1">Grant</th>
<th class="col-md-1">Deny</th>
<th class="col-md-5">{{ trans('admin/groups/titles.permission')}}</th>
<th class="col-md-1">{{ trans('admin/groups/titles.grant')}}</th>
<th class="col-md-1">{{ trans('admin/groups/titles.deny')}}</th>
</tr>
</thead>
@foreach ($permissions as $area => $area_permission)
@ -87,11 +87,11 @@
@endunless
</td>
<td class="col-md-1 permissions-item">
<label for="{{ 'permission['.$localPermission['permission'].']' }}"><span class="sr-only">Allow {{ 'permission['.$localPermission['permission'].']' }}</span></label>
<label for="{{ 'permission['.$localPermission['permission'].']' }}"><span class="sr-only">{{ trans('admin/groups/titles.allow')}} {{ 'permission['.$localPermission['permission'].']' }}</span></label>
{{ Form::radio('permission['.$localPermission['permission'].']', '1',(array_key_exists($localPermission['permission'], $groupPermissions) ? $groupPermissions[$localPermission['permission'] ] == '1' : null),['value'=>"grant", 'class'=>'minimal', 'aria-label'=> 'permission['.$localPermission['permission'].']']) }}
</td>
<td class="col-md-1 permissions-item">
<label for="{{ 'permission['.$localPermission['permission'].']' }}"><span class="sr-only">Deny {{ 'permission['.$localPermission['permission'].']' }}</span></label>
<label for="{{ 'permission['.$localPermission['permission'].']' }}"><span class="sr-only">{{ trans('admin/groups/titles.deny')}} {{ 'permission['.$localPermission['permission'].']' }}</span></label>
{{ Form::radio('permission['.$localPermission['permission'].']', '0',(array_key_exists($localPermission['permission'], $groupPermissions) ? $groupPermissions[$localPermission['permission'] ] == '0' : null),['value'=>"grant", 'class'=>'minimal', 'aria-label'=> 'permission['.$localPermission['permission'].']']) }}
</td>
</tr>
@ -108,11 +108,11 @@
</td>
<td class="col-md-1 permissions-item" style="vertical-align: bottom">
<label for="{{ $area }}"><span class="sr-only">Allow {{ $area }}</span></label>
<label for="{{ $area }}"><span class="sr-only">{{ trans('admin/groups/titles.allow')}} {{ $area }}</span></label>
{{ Form::radio("$area", '1',false,['value'=>"grant", 'class'=>'minimal', 'data-checker-group' => str_slug($area), 'aria-label'=> $area]) }}
</td>
<td class="col-md-1 permissions-item">
<label for="{{ $area }}"><span class="sr-only">Deny {{ $area }}</span></label>
<label for="{{ $area }}"><span class="sr-only">{{ trans('admin/groups/titles.deny')}} {{ $area }}</span></label>
{{ Form::radio("$area", '0',false,['value'=>"deny", 'class'=>'minimal', 'data-checker-group' => str_slug($area), 'aria-label'=> $area]) }}
</td>
</tr>
@ -128,11 +128,11 @@
{{ $this_permission['label'] }}
</td>
<td class="col-md-1 permissions-item">
<label for="{{ 'permission['.$this_permission['permission'].']' }}"><span class="sr-only">Allow {{ 'permission['.$this_permission['permission'].']' }}</span></label>
<label for="{{ 'permission['.$this_permission['permission'].']' }}"><span class="sr-only">{{ trans('admin/groups/titles.allow')}} {{ 'permission['.$this_permission['permission'].']' }}</span></label>
{{ Form::radio('permission['.$this_permission['permission'].']', '1',(array_key_exists($this_permission['permission'], $groupPermissions) ? $groupPermissions[$this_permission['permission'] ] == '1' : null),['class'=>'minimal radiochecker-'.str_slug($area), 'aria-label'=>'permission['.$this_permission['permission'].']']) }}
</td>
<td class="col-md-1 permissions-item">
<label for="{{ 'permission['.$this_permission['permission'].']' }}"><span class="sr-only">Deny {{ 'permission['.$this_permission['permission'].']' }}</span></label>
<label for="{{ 'permission['.$this_permission['permission'].']' }}"><span class="sr-only">{{ trans('admin/groups/titles.deny')}} {{ 'permission['.$this_permission['permission'].']' }}</span></label>
{{ Form::radio('permission['.$this_permission['permission'].']', '0',(array_key_exists($this_permission['permission'], $groupPermissions) ? $groupPermissions[$this_permission['permission'] ] == '0' : null),['class'=>'minimal radiochecker-'.str_slug($area), 'aria-label'=>'permission['.$this_permission['permission'].']']) }}
</td>

View file

@ -57,7 +57,7 @@
</ul>
@else
<p>This group has no permissions.</p>
<p>{{ trans('admin/groups/title.no_permissions') }}</p>
@endif
</div>

View file

@ -57,7 +57,7 @@
<div class="form-group">
<div class="col-sm-offset-3 col-md-9">
<label>
<input type="checkbox" value="1" name="update_location" class="minimal" {{ Request::old('update_location') == '1' ? ' checked="checked"' : '' }}> Update asset location
<input type="checkbox" value="1" name="update_location" class="minimal" {{ Request::old('update_location') == '1' ? ' checked="checked"' : '' }}> {{ trans('admin/hardware/form.asset_location') }}
</label>
@include ('partials.more-info', ['helpText' => trans('help.audit_help'), 'helpPosition' => 'right'])

View file

@ -29,10 +29,10 @@
<thead>
<tr>
<td></td>
<td>ID</td>
<td>Name</td>
<td>Location</td>
<td>Assigned To</td>
<td>{{ trans('admin/hardware/table.id') }}</td>
<td>{{ trans('admin/hardware/table.name') }}</td>
<td>{{ trans('admin/hardware/table.location')}}</td>
<td>{{ trans('admin/hardware/table.assigned_to') }}</td>
</tr>
</thead>
<tbody>
@ -59,7 +59,7 @@
<div class="box-footer text-right">
<a class="btn btn-link" href="{{ URL::previous() }}" method="post" enctype="multipart/form-data">{{ trans('button.cancel') }}</a>
<button type="submit" class="btn btn-success" id="submit-button"><i class="fas fa-check icon-white" aria-hidden="true"></i> {{ trans('general.delete') }}</button>
<button type="submit" class="btn btn-success" id="submit-button"><i class="fas fa-check icon-white" aria-hidden="true"></i> {{ trans('button.delete') }}</button>
</div><!-- /.box-footer -->
</div><!-- /.box -->
</form>

View file

@ -75,12 +75,12 @@
<label for="update_real_loc">
{{ Form::radio('update_real_loc', '1', old('update_real_loc'), ['class'=>'minimal', 'aria-label'=>'update_real_loc']) }}
Update default location AND actual location
{{ trans('admin/hardware/form.asset_location_update_default_current') }}
</label>
<br>
<label for="update_default_loc">
{{ Form::radio('update_real_loc', '0', old('update_real_loc'), ['class'=>'minimal', 'aria-label'=>'update_default_loc']) }}
Update only default location
{{ trans('admin/hardware/form.asset_location_update_default') }}
</label>
</div>
@ -137,13 +137,13 @@
</div>
<div class="col-md-7">
<label class="radio">
<input type="radio" class="minimal" name="requestable" value="1"> Yes
<input type="radio" class="minimal" name="requestable" value="1"> {{ trans('general.yes')}}
</label>
<label class="radio">
<input type="radio" class="minimal" name="requestable" value="0"> No
<input type="radio" class="minimal" name="requestable" value="0"> {{ trans('general.no')}}
</label>
<label class="radio">
<input type="radio" class="minimal" name="requestable" value=""> Do Not Change
<input type="radio" class="minimal" name="requestable" value=""> {{ trans('general.do_not_change')}}
</label>
</div>
</div>

View file

@ -124,7 +124,7 @@
@if ($snipeSettings->slack_endpoint!='')
<i class="fab fa-slack" aria-hidden="true"></i>
A slack message will be sent
{{ trans('general.slack_msg_note')}}
@endif
</div>
</div>

View file

@ -181,7 +181,7 @@
$("#selected_status_status").removeClass('text-danger');
$("#selected_status_status").removeClass('text-warning');
$("#selected_status_status").addClass('text-success');
$("#selected_status_status").html('<i class="fas fa-check"></i> That status is deployable. This asset can be checked out.');
$("#selected_status_status").html('<i class="fas fa-check"></i> {{ trans('admin/hardware/form.asset_deployable')}}');
} else {
@ -189,7 +189,7 @@
$("#selected_status_status").removeClass('text-danger');
$("#selected_status_status").removeClass('text-success');
$("#selected_status_status").addClass('text-warning');
$("#selected_status_status").html('<i class="fas fa-exclamation-triangle"></i> That asset status is not deployable. This asset cannot be checked out. ');
$("#selected_status_status").html('<i class="fas fa-exclamation-triangle"></i> {{ trans('admin/hardware/form.asset_not_deployable')}} ');
}
}
});

View file

@ -2,7 +2,7 @@
{{-- Page title --}}
@section('title')
Import History
{{ trans('general.import-history') }}
@parent
@stop
@ -22,8 +22,8 @@
<div class="box box-default">
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle faa-pulse animated"></i>
<strong>{{ count($status['error']) }} Error Messagess: </strong>
Please see below for errors.
<strong>{{ count($status['error']) }} {{ trans('admin/hardware/general.error_messages') }}</strong> </strong>
{{ trans('admin/hardware/general.alert_details') }}
</div>
</div>
</div>
@ -35,8 +35,8 @@
<div class="box box-default">
<div class="alert alert-success">
<i class="fas fa-check faa-pulse animated"></i>
<strong>{{ count($status['success']) }} Success Messages: </strong>
Please see below for details.
<strong>{{ count($status['success']) }} {{ trans('admin/hardware/general.success_messages') }} </strong>
{{ trans('admin/hardware/general.alert_details') }}
</div>
</div>
</div>
@ -55,18 +55,12 @@
@if (Session::get('message'))
<p class="alert-danger">
You have an error in your CSV file:<br />
{{ trans('admin/hardware/general.csv_error') }}<br />
{{ Session::get('message') }}
</p>
@endif
<p>
Upload a CSV that contains asset history. The assets and users MUST already exist in the system, or they will be skipped. Matching assets for history import happens against the asset tag. We will try to find a matching user based on the user's name you provide, and the criteria you select below. If you do not select any criteria below, it will simply try to match on the username format you configured in the Admin &gt; General Settings.
</p>
<p>Fields included in the CSV must match the headers: <strong>Asset Tag, Name, Checkout Date, Checkin Date</strong>. Any additional fields will be ignored. </p>
<p>Checkin Date: blank or future checkin dates will checkout items to associated user. Excluding the Checkin Date column will create a checkin date with todays date.</p>
{!! trans('admin/hardware/general.import_text') !!}
<div class="form-group">
<label for="first_name" class="col-sm-3 control-label">{{ trans('admin/users/general.usercsv') }}</label>
@ -86,7 +80,7 @@
<div class="col-sm-2">
</div>
<div class="col-sm-10">
{{ Form::checkbox('match_firstnamelastname', '1', Request::old('match_firstnamelastname')) }} Try to match users by firstname.lastname (jane.smith) format
{{ Form::checkbox('match_firstnamelastname', '1', Request::old('match_firstnamelastname')) }} {{ trans('admin/hardware/general.csv_import_match_f-l') }}
</div>
</div>
@ -95,7 +89,7 @@
<div class="col-sm-2">
</div>
<div class="col-sm-10">
{{ Form::checkbox('match_flastname', '1', Request::old('match_flastname')) }} Try to match users by first initial last name (jsmith) format
{{ Form::checkbox('match_flastname', '1', Request::old('match_flastname')) }} {{ trans('admin/hardware/general.csv_import_match_initial_last') }}
</div>
</div>
@ -104,7 +98,7 @@
<div class="col-sm-2">
</div>
<div class="col-sm-10">
{{ Form::checkbox('match_firstname', '1', Request::old('match_firstname')) }} Try to match users by first name (jane) format
{{ Form::checkbox('match_firstname', '1', Request::old('match_firstname')) }} {{ trans('admin/hardware/general.csv_import_match_first') }}
</div>
</div>
@ -113,7 +107,7 @@
<div class="col-sm-2">
</div>
<div class="col-sm-10">
{{ Form::checkbox('match_email', '1', Request::old('match_email')) }} Try to match users by email as username
{{ Form::checkbox('match_email', '1', Request::old('match_email')) }} {{ trans('admin/hardware/general.csv_import_match_email') }}
</div>
</div>
@ -122,7 +116,7 @@
<div class="col-sm-2">
</div>
<div class="col-sm-10">
{{ Form::checkbox('match_username', '1', Request::old('match_username')) }} Try to match users by username
{{ Form::checkbox('match_username', '1', Request::old('match_username')) }} {{ trans('admin/hardware/general.csv_import_match_username') }}
</div>
</div>
@ -158,7 +152,7 @@
<div class="col-md-12">
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title"> {{ count($status['error']) }} Error Messages </h2>
<h2 class="box-title"> {{ count($status['error']) }} {{ trans('admin/hardware/general.error_messages') }} </h2>
</div>
<div class="box-body">
<div style="height : 400px; overflow : auto;">
@ -185,7 +179,7 @@
<div class="col-md-12">
<div class="box box-default">
<div class="box-header with-border">
<h2 class="box-title"> {{ count($status['success']) }} Success Messages </h2>
<h2 class="box-title"> {{ count($status['success']) }} {{ trans('admin/hardware/general.success_messages') }} </h2>
</div>
<div class="box-body">
<div style="height : 400px; overflow : auto;">

View file

@ -43,7 +43,7 @@
@section('header_right')
<a href="{{ route('reports/custom') }}" style="margin-right: 5px;" class="btn btn-default">
Custom Export</a>
{{ trans('admin/hardware/general.custom_export') }}</a>
@can('create', \App\Models\Asset::class)
<a href="{{ route('hardware.create') }}" class="btn btn-primary pull-right"></i> {{ trans('general.create') }}</a>
@endcan
@ -73,14 +73,14 @@
'id' => 'bulkForm']) }}
<label for="bulk_actions"><span class="sr-only">Bulk Actions</span></label>
<label for="bulk_actions"><span class="sr-only">{{ trans('button.bulk_actions') }}</span></label>
<select name="bulk_actions" class="form-control select2" aria-label="bulk_actions">
<option value="edit">{{ trans('button.edit') }}</option>
<option value="delete">{{ trans('button.delete') }}</option>
<option value="labels">{{ trans_choice('button.generate_labels', 2) }}</option>
</select>
<button class="btn btn-primary" id="bulkEdit" disabled>Go</button>
<button class="btn btn-primary" id="bulkEdit" disabled>{{ trans('button.go') }}</button>
{{ Form::close() }}
</div>

View file

@ -51,7 +51,7 @@
<div class="form-group">
<div class="col-sm-offset-3 col-md-9">
<label>
<input type="checkbox" value="1" name="update_location" class="minimal" {{ Request::old('update_location') == '1' ? ' checked="checked"' : '' }}> Update asset location
<input type="checkbox" value="1" name="update_location" class="minimal" {{ Request::old('update_location') == '1' ? ' checked="checked"' : '' }}> {{ trans('admin/hardware/form.asset_location') }}
</label> <a href="#" class="text-dark-gray" tabindex="0" role="button" data-toggle="popover" data-trigger="focus" title="<i class='far fa-life-ring'></i> More Info" data-html="true" data-content="Checking this box will edit the asset record to reflect this new location. Leaving it unchecked will simply note the location in the audit log.<br><br>Note that is this asset is checked out, it will not change the location of the person, asset or location it is checked out to."><i class="far fa-life-ring"></i></a>
</div>
</div>
@ -112,7 +112,7 @@
</tr>
<tr id="audit-loader" style="display: none;">
<td colspan="3">
<i class="fas fa-spinner spin" aria-hidden="true"></i> Processing...
<i class="fas fa-spinner spin" aria-hidden="true"></i> {{ trans('admin/hardware/form.processing_spinner') }}
</td>
</tr>
</thead>

View file

@ -49,8 +49,8 @@
<th class="col-md-2">Item Name</th>
<th class="col-md-2" data-sortable="true">{{ trans('admin/hardware/table.location') }}</th>
<th class="col-md-2" data-sortable="true">{{ trans('admin/hardware/form.expected_checkin') }}</th>
<th class="col-md-3" data-sortable="true">Requesting User</th>
<th class="col-md-2">Requested Date</th>
<th class="col-md-3" data-sortable="true">{{ trans('admin/hardware/form.requesting_user') }}</th>
<th class="col-md-2">{{ trans('admin/hardware/form.requested_date') }}</th>
<th class="col-md-1"></th>
<th class="col-md-1"></th>
</tr>
@ -107,9 +107,9 @@
<td>
@if ($request->itemType() == "asset")
@if ($request->requestable->assigned_to=='')
<a href="{{ url('/') }}/hardware/{{ $request->requestable->id }}/checkout" class="btn btn-sm bg-maroon" data-tooltip="true" title="Check this item out to a user">{{ trans('general.checkout') }}</a>
<a href="{{ url('/') }}/hardware/{{ $request->requestable->id }}/checkout" class="btn btn-sm bg-maroon" data-tooltip="true" title="{{ trans('general.checkout_user_tooltip') }}">{{ trans('general.checkout') }}</a>
@else
<a href="{{ url('/') }}/hardware/{{ $request->requestable->id }}/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="Check this itemi">{{ trans('general.checkin') }}</a>
<a href="{{ url('/') }}/hardware/{{ $request->requestable->id }}/checkin" class="btn btn-sm bg-purple" data-tooltip="true" title="{{ trans('general.checkin_toolip') }}">{{ trans('general.checkin') }}</a>
@endif
@endif

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