Merge branch 'develop' of https://github.com/snipe/snipe-it into develop

This commit is contained in:
snipe 2021-03-30 07:40:14 -07:00
commit 2354e37504
20 changed files with 492 additions and 69 deletions

View file

@ -97,6 +97,7 @@ RUN \
&& mkdir -p "/var/lib/snipeit/keys" && ln -fs "/var/lib/snipeit/keys/oauth-private.key" "/var/www/html/storage/oauth-private.key" \
&& ln -fs "/var/lib/snipeit/keys/oauth-public.key" "/var/www/html/storage/oauth-public.key" \
&& chown docker "/var/lib/snipeit/keys/" \
&& chown -h docker "/var/www/html/storage/*.key" \
&& chmod +x /var/www/html/artisan \
&& echo "Finished setting up application in /var/www/html"

View file

@ -57,6 +57,7 @@ RUN \
&& mkdir -p "/var/lib/snipeit/dumps" && rm -r "/var/www/html/storage/app/backups" && ln -fs "/var/lib/snipeit/dumps" "/var/www/html/storage/app/backups" \
&& mkdir -p "/var/lib/snipeit/keys" && ln -fs "/var/lib/snipeit/keys/oauth-private.key" "/var/www/html/storage/oauth-private.key" \
&& ln -fs "/var/lib/snipeit/keys/oauth-public.key" "/var/www/html/storage/oauth-public.key" \
&& chown -h "/var/www/html/storage/*.key"
&& chown -R apache "/var/lib/snipeit"
# Install composer

View file

@ -0,0 +1,138 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Http\Transformers\LicenseSeatsTransformer;
use App\Models\Asset;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Auth;
use Illuminate\Http\Request;
class LicenseSeatsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request, $licenseId)
{
//
if ($license = License::find($licenseId)) {
$this->authorize('view', $license);
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
->where('license_seats.license_id', $licenseId);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
if ($request->input('sort')=='department') {
$seats->OrderDepartments($order);
} else {
$seats->orderBy('id', $order);
}
$total = $seats->count();
$offset = (($seats) && (request('offset') > $total)) ? 0 : request('offset', 0);
$limit = request('limit', 50);
$seats = $seats->skip($offset)->take($limit)->get();
if ($seats) {
return (new LicenseSeatsTransformer)->transformLicenseSeats($seats, $total);
}
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.does_not_exist')), 200);
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($licenseId, $seatId)
{
//
$this->authorize('view', License::class);
// sanity checks:
// 1. does the license seat exist?
if (!$licenseSeat = LicenseSeat::find($seatId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat not found'));
}
// 2. does the seat belong to the specified license?
if (!$license = $licenseSeat->license()->first() || $license->id != intval($licenseId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license'));
}
return (new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $licenseId
* @param int $seatId
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $licenseId, $seatId)
{
$this->authorize('checkout', License::class);
// sanity checks:
// 1. does the license seat exist?
if (!$licenseSeat = LicenseSeat::find($seatId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat not found'));
}
// 2. does the seat belong to the specified license?
if (!$license = $licenseSeat->license()->first() || $license->id != intval($licenseId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Seat does not belong to the specified license'));
}
$oldUser = $licenseSeat->user()->first();
$oldAsset = $licenseSeat->asset()->first();
// attempt to update the license seat
$licenseSeat->fill($request->all());
$licenseSeat->user_id = Auth::user()->id;
// check if this update is a checkin operation
// 1. are relevant fields touched at all?
$touched = $licenseSeat->isDirty('assigned_to') || $licenseSeat->isDirty('asset_id');
// 2. are they cleared? if yes then this is a checkin operation
$is_checkin = ($touched && $licenseSeat->assigned_to === null && $licenseSeat->asset_id === null);
if (!$touched) {
// nothing to update
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
if ($licenseSeat->save()) {
// the logging functions expect only one "target". if both asset and user are present in the request,
// we simply let assets take precedence over users...
$changes = $licenseSeat->getChanges();
if (array_key_exists('assigned_to', $changes)) {
$target = $is_checkin ? $oldUser : User::find($changes['assigned_to']);
}
if (array_key_exists('asset_id', $changes)) {
$target = $is_checkin ? $oldAsset : Asset::find($changes['asset_id']);
}
if ($is_checkin) {
$licenseSeat->logCheckin($target, $request->input('note'));
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
// in this case, relevant fields are touched but it's not a checkin operation. so it must be a checkout operation.
$licenseSeat->logCheckout($request->input('note'), $target);
return response()->json(Helper::formatStandardApiResponse('success', $licenseSeat, trans('admin/licenses/message.update.success')));
}
return Helper::formatStandardApiResponse('error', null, $licenseSeat->getErrors());
}
}

View file

@ -238,50 +238,6 @@ class LicensesController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.assoc_users')));
}
/**
* Get license seat listing
*
* @author [A. Gianotto] [<snipe@snipe.net>]
* @since [v1.0]
* @param int $licenseId
* @return \Illuminate\Contracts\View\View
*/
public function seats(Request $request, $licenseId)
{
if ($license = License::find($licenseId)) {
$this->authorize('view', $license);
$seats = LicenseSeat::with('license', 'user', 'asset', 'user.department')
->where('license_seats.license_id', $licenseId);
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
if ($request->input('sort')=='department') {
$seats->OrderDepartments($order);
} else {
$seats->orderBy('id', $order);
}
$offset = (($seats) && (request('offset') > $seats->count())) ? 0 : request('offset', 0);
$limit = request('limit', 50);
$total = $seats->count();
$seats = $seats->skip($offset)->take($limit)->get();
if ($seats) {
return (new LicenseSeatsTransformer)->transformLicenseSeats($seats, $total);
}
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/licenses/message.does_not_exist')), 200);
}
/**
* Gets a paginated collection for the select2 menus
*

View file

@ -48,10 +48,9 @@ class ProfileController extends Controller
$user->last_name = $request->input('last_name');
$user->website = $request->input('website');
$user->gravatar = $request->input('gravatar');
$user->skin = $request->input('skin');
$user->phone = $request->input('phone');
if (!config('app.lock_passwords')) {
$user->locale = $request->input('locale', 'en');
}

View file

@ -400,6 +400,7 @@ class SettingsController extends Controller
$setting->version_footer = $request->input('version_footer');
$setting->footer_text = $request->input('footer_text');
$setting->skin = $request->input('skin');
$setting->allow_user_skin = $request->input('allow_user_skin');
$setting->show_url_in_emails = $request->input('show_url_in_emails', '0');
$setting->logo_print_assets = $request->input('logo_print_assets', '0');

View file

@ -20,12 +20,11 @@ class LicenseSeatsTransformer
return (new DatatablesTransformer)->transformDatatables($array, $total);
}
public function transformLicenseSeat (LicenseSeat $seat, $seat_count)
public function transformLicenseSeat (LicenseSeat $seat, $seat_count=0)
{
$array = [
'id' => (int) $seat->id,
'license_id' => (int) $seat->license->id,
'name' => 'Seat '.$seat_count,
'assigned_user' => ($seat->user) ? [
'id' => (int) $seat->user->id,
'name'=> e($seat->user->present()->fullName),
@ -49,6 +48,10 @@ class LicenseSeatsTransformer
'user_can_checkout' => (($seat->assigned_to=='') && ($seat->asset_id=='')),
];
if($seat_count != 0) {
$array['name'] = 'Seat '.$seat_count;
}
$permissions_array['available_actions'] = [
'checkout' => Gate::allows('checkout', License::class),
'checkin' => Gate::allows('checkin', License::class),

View file

@ -20,6 +20,16 @@ class LicenseSeat extends SnipeModel implements ICompanyableChild
protected $guarded = 'id';
protected $table = 'license_seats';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'assigned_to',
'asset_id'
];
use Acceptable;
public function getCompanyableParents()

View file

@ -129,7 +129,13 @@ class LdapAd extends LdapAdConfiguration
$login_username = $username;
}
if ($this->ldap->auth()->attempt($login_username, $password, true) === false) {
if ($this->ldapConfig['username'] && $this->ldapConfig['password']) {
$bind_as_user = false;
} else {
$bind_as_user = true;
}
if ($this->ldap->auth()->attempt($login_username, $password, $bind_as_user) === false) {
throw new Exception('Unable to validate user credentials!');
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddUserSkinSetting extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Update the users table
Schema::table('users', function ($table) {
$table->string('skin')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Update the users table
Schema::table('users', function ($table) {
$table->dropColumn('skin');
});
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddSettingAllowUserSkin extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Update the users table
Schema::table('settings', function ($table) {
$table->boolean('allow_user_skin')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Update the users table
Schema::table('settings', function ($table) {
$table->dropColumn('allow_user_skin');
});
}
}

View file

@ -14,6 +14,8 @@ return array(
'alerts_enabled' => 'Email Alerts Enabled',
'alert_interval' => 'Expiring Alerts Threshold (in days)',
'alert_inv_threshold' => 'Inventory Alert Threshold',
'allow_user_skin' => 'Allow user skin',
'allow_user_skin_help_text' => 'Checking this box will allow a user to change the site skin for himself.' ,
'asset_ids' => 'Asset IDs',
'audit_interval' => 'Audit Interval',
'audit_interval_help' => 'If you are required to regularly physically audit your assets, enter the interval in months.',

View file

@ -582,3 +582,34 @@ Form::macro('skin', function ($name = "skin", $selected = null, $class = null) {
return $select;
});
Form::macro('user_skin', function ($name = "skin", $selected = null, $class = null) {
$formats = array(
'' => 'Site Default',
'blue' => 'Default Blue',
'blue-dark' => 'Blue (Dark Mode)',
'green' => 'Green Dark',
'green-dark' => 'Green (Dark Mode)',
'red' => 'Red Dark',
'red-dark' => 'Red (Dark Mode)',
'orange' => 'Orange Dark',
'orange-dark' => 'Orange (Dark Mode)',
'black' => 'Black',
'black-dark' => 'Black (Dark Mode)',
'purple' => 'Purple',
'purple-dark' => 'Purple (Dark Mode)',
'yellow' => 'Yellow',
'yellow-dark' => 'Yellow (Dark Mode)',
'contrast' => 'High Contrast',
);
$select = '<select name="'.$name.'" class="'.$class.'" style="width: 250px">';
foreach ($formats as $format => $label) {
$select .= '<option value="'.$format.'"'.($selected == $format ? ' selected="selected"' : '').'>'.$label.'</option> '."\n";
}
$select .= '</select>';
return $select;
});

View file

@ -58,6 +58,17 @@
</div>
</div>
@if ($snipeSettings->allow_user_skin=='1')
<!-- Skin -->
<div class="form-group {{ $errors->has('skin') ? 'error' : '' }}">
<label for="website" class="col-md-3 control-label">{{ Form::label('skin', trans('general.skin')) }}</label>
<div class="col-md-8">
{!! Form::user_skin('skin', Input::old('skin', $user->skin), 'select2') !!}
{!! $errors->first('skin', '<span class="alert-msg">:message</span>') !!}
</div>
</div>
@endif
<!-- Phone -->
<div class="form-group {{ $errors->has('phone') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="phone">{{ trans('admin/users/table.phone') }}</label>
@ -67,8 +78,6 @@
</div>
</div>
<!-- Website URL -->
<div class="form-group {{ $errors->has('website') ? ' has-error' : '' }}">
<label for="website" class="col-md-3 control-label">{{ trans('general.website') }}</label>
@ -114,8 +123,6 @@
<!-- Two factor opt in -->
@if ($snipeSettings->two_factor_enabled=='1')
<div class="form-group {{ $errors->has('avatar') ? 'has-error' : '' }}">

View file

@ -28,12 +28,12 @@
</script>
{{-- stylesheets --}}
<link rel="stylesheet" href="{{ url(mix('css/dist/all.css')) }}">
<link rel="stylesheet" href="{{ mix('css/all.css') }}">
@if (($snipeSettings) && ($snipeSettings->allow_user_skin==1) && Auth::check() && Auth::user()->present()->skin != '')
<link rel="stylesheet" href="{{ mix('css/skins/skin-'.Auth::user()->present()->skin.'.min.css') }}">
@elseif (($snipeSettings) && ($snipeSettings->skin!=''))
<link rel="stylesheet" href="{{ url(mix('css/dist/skins/skin-'.($snipeSettings->skin!='' ? $snipeSettings->skin : 'blue').'.css')) }}">
@endif
{{-- page level css --}}
@stack('css')

View file

@ -350,7 +350,7 @@
data-sort-order="asc"
data-sort-name="name"
class="table table-striped snipe-table"
data-url="{{ route('api.license.seats', $license->id) }}"
data-url="{{ route('api.licenses.seats.index', $license->id) }}"
data-export-options='{
"fileName": "export-seats-{{ str_slug($license->name) }}-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]

View file

@ -41,7 +41,6 @@
iconsPrefix: 'fa',
cookie: true,
cookieExpire: '2y',
cookieIdTable: '{{ Route::currentRouteName() }}',
mobileResponsive: true,
maintainSelected: true,
trimOnSearch: false,

View file

@ -150,7 +150,7 @@
</div>
</div>
<!-- Email format -->
<!-- Skin -->
<div class="form-group {{ $errors->has('skin') ? 'error' : '' }}">
<div class="col-md-3">
{{ Form::label('skin', trans('general.skin')) }}
@ -161,6 +161,17 @@
</div>
</div>
<!-- Allow User Skin -->
<div class="form-group">
<div class="col-md-3">
{{ Form::label('allow_user_skin', trans('admin/settings/general.allow_user_skin')) }}
</div>
<div class="col-md-9">
{{ Form::checkbox('allow_user_skin', '1', Input::old('allow_user_skin', $setting->allow_user_skin),array('class' => 'minimal')) }}
{{ trans('general.yes') }}
<p class="help-block">{{ trans('admin/settings/general.allow_user_skin_help_text') }}</p>
</div>
</div>
<!-- Custom css -->
<div class="form-group {{ $errors->has('custom_css') ? 'error' : '' }}">

View file

@ -166,7 +166,6 @@ Route::group(['prefix' => 'v1','namespace' => 'Api', 'middleware' => 'auth:api']
/*--- Departments API ---*/
/*--- Suppliers API ---*/
Route::group(['prefix' => 'departments'], function () {
@ -496,11 +495,6 @@ Route::group(['prefix' => 'v1','namespace' => 'Api', 'middleware' => 'auth:api']
/*--- Licenses API ---*/
Route::group(['prefix' => 'licenses'], function () {
Route::get('{licenseId}/seats', [
'as' => 'api.license.seats',
'uses' => 'LicensesController@seats'
]);
Route::get('selectlist',
[
'as' => 'api.licenses.selectlist',
@ -525,7 +519,18 @@ Route::group(['prefix' => 'v1','namespace' => 'Api', 'middleware' => 'auth:api']
]
); // Licenses resource
Route::resource('licenses.seats', 'LicenseSeatsController',
[
'names' =>
[
'index' => 'api.licenses.seats.index',
'show' => 'api.licenses.seats.show',
'update' => 'api.licenses.seats.update'
],
'except' => ['create', 'edit', 'destroy', 'store'],
'parameters' => ['licenseseat' => 'licenseseat_id']
]
); // Licenseseats resource
/*--- Locations API ---*/

View file

@ -0,0 +1,185 @@
<?php
use App\Http\Transformers\LicenseSeatsTransformer;
use App\Models\Asset;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
class ApiLicenseSeatsCest
{
protected $license;
protected $timeFormat;
public function _before(ApiTester $I)
{
$this->user = \App\Models\User::find(1);
$I->haveHttpHeader('Accept', 'application/json');
$I->amBearerAuthenticated($I->getToken($this->user));
}
/** @test */
public function indexLicenseSeats(ApiTester $I)
{
$I->wantTo('Get a list of license seats for a specific license');
// call
$I->sendGET('/licenses/1/seats?limit=10&order=desc');
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
// sample verify
$licenseSeats = App\Models\LicenseSeat::where('license_id', 1)
->orderBy('id','desc')->take(10)->get();
// pick a random seat
$licenseSeat = $licenseSeats->random();
// need the index in the original list so that the "name" field is determined correctly
$licenseSeatNumber = 0;
foreach($licenseSeats as $index=>$seat) {
if ($licenseSeat === $seat) {
$licenseSeatNumber = $index+1;
}
}
$I->seeResponseContainsJson($I->removeTimestamps((new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat, $licenseSeatNumber)));
}
/** @test */
public function showLicenseSeat(ApiTester $I)
{
$I->wantTo('Get a license seat');
// call
$I->sendGET('/licenses/1/seats/10');
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
// sample verify
$licenseSeat = App\Models\LicenseSeat::findOrFail(10);
$I->seeResponseContainsJson($I->removeTimestamps((new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat)));
}
/** @test */
public function checkoutLicenseSeatToUser(ApiTester $I)
{
$I->wantTo('Checkout a license seat to a user');
$user = App\Models\User::all()->random();
$licenseSeat = App\Models\LicenseSeat::all()->random();
$endpoint = '/licenses/'.$licenseSeat->license_id.'/seats/'.$licenseSeat->id;
$data = [
'assigned_to' => $user->id,
'note' => 'Test Checkout to User via API'
];
// update
$I->sendPATCH($endpoint, $data);
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
$response = json_decode($I->grabResponse());
$I->assertEquals('success', $response->status);
$I->assertEquals(trans('admin/licenses/message.update.success'), $response->messages);
$I->assertEquals($licenseSeat->license_id, $response->payload->license_id); // license id does not change
$I->assertEquals($licenseSeat->id, $response->payload->id); // license seat id does not change
// verify
$licenseSeat = $licenseSeat->fresh();
$I->sendGET($endpoint);
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
$I->seeResponseContainsJson($I->removeTimestamps((new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat)));
// verify that the last logged action is a checkout
$I->sendGET('/reports/activity?item_type=license&limit=1&item_id='.$licenseSeat->license_id);
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
$I->seeResponseContainsJson([
"action_type" => "checkout"
]);
}
/** @test */
public function checkoutLicenseSeatToAsset(ApiTester $I)
{
$I->wantTo('Checkout a license seat to an asset');
$asset = App\Models\Asset::all()->random();
$licenseSeat = App\Models\LicenseSeat::all()->random();
$endpoint = '/licenses/'.$licenseSeat->license_id.'/seats/'.$licenseSeat->id;
$data = [
'asset_id' => $asset->id,
'note' => 'Test Checkout to Asset via API'
];
// update
$I->sendPATCH($endpoint, $data);
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
$response = json_decode($I->grabResponse());
$I->assertEquals('success', $response->status);
$I->assertEquals(trans('admin/licenses/message.update.success'), $response->messages);
$I->assertEquals($licenseSeat->license_id, $response->payload->license_id); // license id does not change
$I->assertEquals($licenseSeat->id, $response->payload->id); // license seat id does not change
// verify
$licenseSeat = $licenseSeat->fresh();
$I->sendGET($endpoint);
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
$I->seeResponseContainsJson($I->removeTimestamps((new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat)));
// verify that the last logged action is a checkout
$I->sendGET('/reports/activity?item_type=license&limit=1&item_id='.$licenseSeat->license_id);
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
$I->seeResponseContainsJson([
"action_type" => "checkout"
]);
}
/** @test */
public function checkoutLicenseSeatToUserAndAsset(ApiTester $I)
{
$I->wantTo('Checkout a license seat to a user AND an asset');
$asset = App\Models\Asset::all()->random();
$user = App\Models\User::all()->random();
$licenseSeat = App\Models\LicenseSeat::all()->random();
$endpoint = '/licenses/'.$licenseSeat->license_id.'/seats/'.$licenseSeat->id;
$data = [
'asset_id' => $asset->id,
'assigned_to' => $user->id,
'note' => 'Test Checkout to User and Asset via API'
];
// update
$I->sendPATCH($endpoint, $data);
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
$response = json_decode($I->grabResponse());
$I->assertEquals('success', $response->status);
$I->assertEquals(trans('admin/licenses/message.update.success'), $response->messages);
$I->assertEquals($licenseSeat->license_id, $response->payload->license_id); // license id does not change
$I->assertEquals($licenseSeat->id, $response->payload->id); // license seat id does not change
// verify
$licenseSeat = $licenseSeat->fresh();
$I->sendGET($endpoint);
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
$I->seeResponseContainsJson($I->removeTimestamps((new LicenseSeatsTransformer)->transformLicenseSeat($licenseSeat)));
// verify that the last logged action is a checkout
$I->sendGET('/reports/activity?item_type=license&limit=1&item_id='.$licenseSeat->license_id);
$I->seeResponseIsJson();
$I->seeResponseCodeIs(200);
$I->seeResponseContainsJson([
"action_type" => "checkout"
]);
}
}