Implement Entry Copy/Move
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m39s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m55s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
All checks were successful
Create Docker Image / Test Application (x86_64) (push) Successful in 28s
Create Docker Image / Build Docker Image (x86_64) (push) Successful in 1m39s
Create Docker Image / Build Docker Image (arm64) (push) Successful in 2m55s
Create Docker Image / Final Docker Image Manifest (push) Successful in 10s
This commit is contained in:
parent
8f1c628324
commit
31524418b9
@ -509,4 +509,15 @@ final class Server
|
|||||||
{
|
{
|
||||||
return Arr::get($this->rootDSE->subschemasubentry,0);
|
return Arr::get($this->rootDSE->subschemasubentry,0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function subordinates(string $dn,array $attrs=['dn']): ?LDAPCollection
|
||||||
|
{
|
||||||
|
return $this
|
||||||
|
->get(
|
||||||
|
dn: $dn,
|
||||||
|
attrs: array_merge($attrs,[]))
|
||||||
|
->rawFilter('(hassubordinates=TRUE)')
|
||||||
|
->search()
|
||||||
|
->get() ?: NULL;
|
||||||
|
}
|
||||||
}
|
}
|
@ -113,4 +113,27 @@ class AjaxController extends Controller
|
|||||||
'may' => $oc->getMayAttrs()->pluck('name'),
|
'may' => $oc->getMayAttrs()->pluck('name'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function subordinates(?string $dn=NULL): array
|
||||||
|
{
|
||||||
|
$dn = $dn ? Crypt::decryptString($dn) : '';
|
||||||
|
|
||||||
|
// Sometimes our key has a command, so we'll ignore it
|
||||||
|
if (str_starts_with($dn,'*') && ($x=strpos($dn,'|')))
|
||||||
|
$dn = substr($dn,$x+1);
|
||||||
|
|
||||||
|
$result = collect();
|
||||||
|
// If no DN, we'll find all children
|
||||||
|
if (! $dn)
|
||||||
|
foreach (Server::baseDNs() as $base)
|
||||||
|
$result = $result->merge(config('server')
|
||||||
|
->subordinates($base->getDN()));
|
||||||
|
else
|
||||||
|
$result = config('server')
|
||||||
|
->subordinates(collect(explode(',',$dn))->last());
|
||||||
|
|
||||||
|
return
|
||||||
|
$result->map(fn($item)=>['id'=>$item->getDNSecure(),'value'=>$item->getDN()])
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
}
|
}
|
@ -112,6 +112,60 @@ class HomeController extends Controller
|
|||||||
->with('updated',FALSE);
|
->with('updated',FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function entry_copy_move(Request $request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View
|
||||||
|
{
|
||||||
|
$from_dn = Crypt::decryptString($request->post('dn'));
|
||||||
|
Log::info(sprintf('%s:Renaming [%s] to [%s]',self::LOGKEY,$from_dn,$request->post('to_dn')));
|
||||||
|
|
||||||
|
$o = clone config('server')->fetch($from_dn);
|
||||||
|
|
||||||
|
if (! $o)
|
||||||
|
return back()
|
||||||
|
->withInput()
|
||||||
|
->with('note',__('DN doesnt exist'));
|
||||||
|
|
||||||
|
$o->setDN($request->post('to_dn'));
|
||||||
|
$o->exists = FALSE;
|
||||||
|
|
||||||
|
// Add the RDN attribute to match the new RDN
|
||||||
|
$rdn = collect(explode(',',$request->post('to_dn')))->first();
|
||||||
|
|
||||||
|
list($attr,$value) = explode('=',$rdn);
|
||||||
|
$o->{$attr} = [Entry::TAG_NOTAG => $o->getObject($attr)->tagValuesOld(Entry::TAG_NOTAG)->push($value)->unique()];
|
||||||
|
|
||||||
|
Log::info(sprintf('%s:Copying [%s] to [%s]',self::LOGKEY,$from_dn,$o->getDN()));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$o->save();
|
||||||
|
|
||||||
|
} catch (LdapRecordException $e) {
|
||||||
|
return Redirect::to('/')
|
||||||
|
->withInput(['_key'=>Crypt::encryptString('*dn|'.$from_dn)])
|
||||||
|
->with('failed',sprintf('%s: %s - %s: %s',
|
||||||
|
__('LDAP Server Error Code'),
|
||||||
|
$e->getDetailedError()?->getErrorCode() ?: $e->getMessage(),
|
||||||
|
$e->getDetailedError()?->getErrorMessage() ?: $e->getFile(),
|
||||||
|
$e->getDetailedError()?->getDiagnosticMessage() ?: $e->getLine(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->post('delete') && $request->post('delete') === '1') {
|
||||||
|
Log::info(sprintf('%s:Deleting [%s] after copy',self::LOGKEY,$from_dn));
|
||||||
|
|
||||||
|
$x = $this->entry_delete($request);
|
||||||
|
|
||||||
|
return ($x->getSession()->has('success'))
|
||||||
|
? Redirect::to('/')
|
||||||
|
->withInput(['_key'=>Crypt::encryptString('*dn|'.$o->getDN())])
|
||||||
|
->with('success',__('Entry copied and deleted'))
|
||||||
|
: $x;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Redirect::to('/')
|
||||||
|
->withInput(['_key'=>Crypt::encryptString('*dn|'.$o->getDN())])
|
||||||
|
->with('success',__('Entry copied'));
|
||||||
|
}
|
||||||
|
|
||||||
public function entry_create(EntryAddRequest $request): \Illuminate\Http\RedirectResponse
|
public function entry_create(EntryAddRequest $request): \Illuminate\Http\RedirectResponse
|
||||||
{
|
{
|
||||||
$key = $this->request_key($request,collect(old()));
|
$key = $this->request_key($request,collect(old()));
|
||||||
@ -154,7 +208,7 @@ class HomeController extends Controller
|
|||||||
->with('failed',sprintf('%s: %s - %s: %s',
|
->with('failed',sprintf('%s: %s - %s: %s',
|
||||||
__('LDAP Server Error Code'),
|
__('LDAP Server Error Code'),
|
||||||
$e->getDetailedError()->getErrorCode(),
|
$e->getDetailedError()->getErrorCode(),
|
||||||
__($e->getDetailedError()->getErrorMessage()),
|
$e->getDetailedError()->getErrorMessage(),
|
||||||
$e->getDetailedError()->getDiagnosticMessage(),
|
$e->getDetailedError()->getDiagnosticMessage(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -377,7 +431,7 @@ class HomeController extends Controller
|
|||||||
->with('failed',sprintf('%s: %s - %s: %s',
|
->with('failed',sprintf('%s: %s - %s: %s',
|
||||||
__('LDAP Server Error Code'),
|
__('LDAP Server Error Code'),
|
||||||
$e->getDetailedError()->getErrorCode(),
|
$e->getDetailedError()->getErrorCode(),
|
||||||
__($e->getDetailedError()->getErrorMessage()),
|
$e->getDetailedError()->getErrorMessage(),
|
||||||
$e->getDetailedError()->getDiagnosticMessage(),
|
$e->getDetailedError()->getDiagnosticMessage(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -435,8 +489,8 @@ class HomeController extends Controller
|
|||||||
->with('dn',$key['dn'])
|
->with('dn',$key['dn'])
|
||||||
->with('o',$o)
|
->with('o',$o)
|
||||||
->with('page_actions',collect([
|
->with('page_actions',collect([
|
||||||
'copy'=>FALSE,
|
|
||||||
'create'=>($x=($o->getObjects()->except('entryuuid')->count() > 0)),
|
'create'=>($x=($o->getObjects()->except('entryuuid')->count() > 0)),
|
||||||
|
'copy'=>$x,
|
||||||
'delete'=>(! is_null($xx=$o->getObject('hassubordinates')->value)) && ($xx === 'FALSE'),
|
'delete'=>(! is_null($xx=$o->getObject('hassubordinates')->value)) && ($xx === 'FALSE'),
|
||||||
'edit'=>$x,
|
'edit'=>$x,
|
||||||
'export'=>$x,
|
'export'=>$x,
|
||||||
|
@ -92,11 +92,12 @@ class Entry extends Model
|
|||||||
public function getAttributes(): array
|
public function getAttributes(): array
|
||||||
{
|
{
|
||||||
return $this->objects
|
return $this->objects
|
||||||
|
->filter(fn($item)=>(! $item->is_internal))
|
||||||
->flatMap(fn($item)=>
|
->flatMap(fn($item)=>
|
||||||
($item->no_attr_tags)
|
$item->no_attr_tags
|
||||||
? [strtolower($item->name)=>$item->values]
|
? [strtolower($item->name)=>$item->values]
|
||||||
: $item->values
|
: $item->values
|
||||||
->flatMap(fn($v,$k)=>[strtolower($item->name.($k !== self::TAG_NOTAG ? ';'.$k : ''))=>$v]))
|
->flatMap(fn($v,$k)=>[strtolower($item->name.(($k !== self::TAG_NOTAG) ? ';'.$k : ''))=>$v]))
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
public/css/custom.css
vendored
7
public/css/custom.css
vendored
@ -19,7 +19,7 @@ attribute#objectclass .input-group-end:not(input.form-control) {
|
|||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group:first-child:not(.select-group) .select2-container--bootstrap-5 .select2-selection {
|
.input-group:first-child:not(.select-group):not(:last-child) .select2-container--bootstrap-5 .select2-selection {
|
||||||
border-bottom-right-radius: unset;
|
border-bottom-right-radius: unset;
|
||||||
border-top-right-radius: unset;
|
border-top-right-radius: unset;
|
||||||
}
|
}
|
||||||
@ -97,4 +97,9 @@ input.form-control.input-group-end {
|
|||||||
/* Square UL items */
|
/* Square UL items */
|
||||||
ul.square {
|
ul.square {
|
||||||
list-style-type: square;
|
list-style-type: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix select2 selections when set up with d-none */
|
||||||
|
select[class*="d-none"] + span.select2 {
|
||||||
|
display: none;
|
||||||
}
|
}
|
@ -23,7 +23,9 @@
|
|||||||
@endif
|
@endif
|
||||||
@if($page_actions->get('copy'))
|
@if($page_actions->get('copy'))
|
||||||
<li>
|
<li>
|
||||||
<button class="btn btn-outline-dark p-1 m-1" id="entry-copy-move" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Copy/Move')" disabled><i class="fas fa-fw fa-copy fs-5"></i></button>
|
<span id="entry-copy-move" data-bs-toggle="modal" data-bs-target="#page-modal">
|
||||||
|
<button class="btn btn-outline-dark p-1 m-1" data-bs-toggle="tooltip" data-bs-placement="bottom" title="@lang('Copy/Move')"><i class="fas fa-fw fa-copy fs-5"></i></button>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
@endif
|
@endif
|
||||||
@if($page_actions->get('edit'))
|
@if($page_actions->get('edit'))
|
||||||
@ -226,6 +228,25 @@
|
|||||||
var that = $(this).find('.modal-content');
|
var that = $(this).find('.modal-content');
|
||||||
|
|
||||||
switch ($(item.relatedTarget).attr('id')) {
|
switch ($(item.relatedTarget).attr('id')) {
|
||||||
|
case 'entry-copy-move':
|
||||||
|
$.ajax({
|
||||||
|
method: 'GET',
|
||||||
|
url: '{{ url('modal/copy-move') }}/'+dn,
|
||||||
|
dataType: 'html',
|
||||||
|
cache: false,
|
||||||
|
beforeSend: function() {
|
||||||
|
that.empty().append('<span class="p-3"><i class="fas fa-3x fa-spinner fa-pulse"></i></span>');
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
that.empty().html(data);
|
||||||
|
},
|
||||||
|
error: function(e) {
|
||||||
|
if (e.status !== 412)
|
||||||
|
alert('That didnt work? Please try again....');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
case 'entry-delete':
|
case 'entry-delete':
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
176
resources/views/modals/entry-copy-move.blade.php
Normal file
176
resources/views/modals/entry-copy-move.blade.php
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<div class="modal-header bg-dark text-white">
|
||||||
|
<h1 class="modal-title fs-5">
|
||||||
|
<i class="fas fa-fw fa-exclamation-triangle"></i> @lang('Rename') <strong>{{ $x=Crypt::decryptString($dn) }}</strong>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<span>
|
||||||
|
@lang('New') DN: <strong><span id="newdn" class="fs-4 opacity-50"><small class="fs-5">[@lang('Select Base')]</small></span></strong>
|
||||||
|
</span>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<form id="entry-rename-form" method="POST" action="{{ url('entry/copy-move') }}">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="dn" value="{{ $dn }}">
|
||||||
|
<input type="hidden" name="to_dn" value="">
|
||||||
|
|
||||||
|
<div class="row pb-3">
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="delete-checkbox" name="delete" value="1">
|
||||||
|
<label class="form-check-label" for="delete-checkbox">
|
||||||
|
<i class="fas fa-fw fa-trash"></i> @lang('Delete after Copy')
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text" id="label">@lang('Select Base of Entry')</span>
|
||||||
|
<input type="text" id="rdn" class="form-control d-none" style="width:20%;" placeholder="{{ $rdn=collect(explode(',',$x))->first() }}" value="{{ $rdn }}">
|
||||||
|
<span class="input-group-text p-1 d-none">,</span>
|
||||||
|
<select class="form-select w-25 d-none" id="rename-subbase" disabled style="width:30%;"></select>
|
||||||
|
<span class="input-group-text p-1 d-none">,</span>
|
||||||
|
<select class="form-select w-25" id="rename-base" style="width:30%;" disabled></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<x-modal.close/>
|
||||||
|
<button id="entry-rename" type="button" class="btn btn-sm btn-primary">@lang('Copy')</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
function refreshdn(value) {
|
||||||
|
$('#newdn')
|
||||||
|
.removeClass('opacity-50')
|
||||||
|
.empty()
|
||||||
|
.append(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
var rdn = '{{ $rdn }}';
|
||||||
|
var base = '';
|
||||||
|
|
||||||
|
var that=$('#newdn');
|
||||||
|
|
||||||
|
// Get our bases
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST',
|
||||||
|
url: '{{ url('ajax/subordinates') }}',
|
||||||
|
dataType: 'json',
|
||||||
|
cache: false,
|
||||||
|
beforeSend: function() {
|
||||||
|
that.empty().append('<span class="p-3"><i class="fas fa-xs fa-spinner fa-pulse"></i></span>');
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
that.empty().html('<small class="fs-5">[@lang('Select Base')]</small>');
|
||||||
|
|
||||||
|
$('#rename-base').children().remove();
|
||||||
|
$('#rename-base').append('<option value=""></option>');
|
||||||
|
data.forEach(function(item) {
|
||||||
|
$('#rename-base').append('<option value="'+item.id+'">'+item.value+'</option>');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rename-base').prop('disabled',false);
|
||||||
|
},
|
||||||
|
error: function(e) {
|
||||||
|
if (e.status !== 412)
|
||||||
|
alert('That didnt work? Please try again....');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// The base DN container
|
||||||
|
$('#rename-base').select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
dropdownAutoWidth: true,
|
||||||
|
width: 'style',
|
||||||
|
allowClear: false,
|
||||||
|
placeholder: 'Choose Base',
|
||||||
|
})
|
||||||
|
.on('change',function() {
|
||||||
|
$(this).prev().removeClass('d-none');
|
||||||
|
$('#rename-subbase').removeClass('d-none')
|
||||||
|
.prev().removeClass('d-none')
|
||||||
|
.prev().removeClass('d-none');
|
||||||
|
$('#label').empty().append("@lang('Complete Path')");
|
||||||
|
|
||||||
|
base = '';
|
||||||
|
if (x=$('#rename-subbase option:selected').text())
|
||||||
|
base += x+',';
|
||||||
|
base += $('#rename-base option:selected').text();
|
||||||
|
|
||||||
|
refreshdn(rdn+','+base);
|
||||||
|
var newdn = '';
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
method: 'POST',
|
||||||
|
url:'{{ url('ajax/children') }}',
|
||||||
|
data: {_key: $(this).val() },
|
||||||
|
dataType: 'json',
|
||||||
|
cache: false,
|
||||||
|
beforeSend: function() {
|
||||||
|
newdn = that.text();
|
||||||
|
console.log(newdn);
|
||||||
|
that.empty().append('<span class="p-3"><i class="fas fa-xs fa-spinner fa-pulse"></i></span>');
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
that.empty().text(newdn);
|
||||||
|
$('#rename-subbase').children().remove();
|
||||||
|
$('#rename-subbase').append('<option value=""></option>');
|
||||||
|
data.forEach(function(item) {
|
||||||
|
$('#rename-subbase').append('<option value="'+item.item+'">'+item.title+'</option>');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rename-subbase').prop('disabled',false);
|
||||||
|
},
|
||||||
|
error: function(e) {
|
||||||
|
if (e.status !== 412)
|
||||||
|
alert('That didnt work? Please try again....');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optional make a child a new branch
|
||||||
|
$('#rename-subbase').select2({
|
||||||
|
theme: 'bootstrap-5',
|
||||||
|
dropdownAutoWidth: true,
|
||||||
|
width: 'style',
|
||||||
|
allowClear: true,
|
||||||
|
placeholder: 'New Subordinate (optional)',
|
||||||
|
})
|
||||||
|
.on('change',function(item) {
|
||||||
|
base = '';
|
||||||
|
if (x=$('#rename-subbase option:selected').text())
|
||||||
|
base += x+',';
|
||||||
|
base += $('#rename-base option:selected').text();
|
||||||
|
|
||||||
|
refreshdn(rdn+','+base);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Complete the RDN
|
||||||
|
$('#rdn').on('input',function(item) {
|
||||||
|
rdn = $(this).val();
|
||||||
|
refreshdn(rdn+','+base);
|
||||||
|
|
||||||
|
$('button[id=entry-rename]').attr('disabled',! rdn.includes('='));
|
||||||
|
})
|
||||||
|
|
||||||
|
// The submit button text
|
||||||
|
$('input#delete-checkbox').on('change',function() {
|
||||||
|
$('button#entry-rename').html($(this).prop('checked') ? '{{ __('Move') }}' : '{{ __('Copy') }}');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
$('button[id=entry-rename]').on('click',function() {
|
||||||
|
$('input[name=to_dn]').val(rdn+','+base);
|
||||||
|
$('form#entry-rename-form').submit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
@ -43,18 +43,21 @@ Route::controller(HomeController::class)->group(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Route::match(['get','post'],'entry/add','entry_add');
|
Route::match(['get','post'],'entry/add','entry_add');
|
||||||
|
Route::post('entry/attr/add/{id}','entry_attr_add');
|
||||||
Route::post('entry/create','entry_create');
|
Route::post('entry/create','entry_create');
|
||||||
|
Route::post('entry/copy-move','entry_copy_move');
|
||||||
Route::post('entry/delete','entry_delete');
|
Route::post('entry/delete','entry_delete');
|
||||||
Route::get('entry/export/{id}','entry_export');
|
Route::get('entry/export/{id}','entry_export');
|
||||||
Route::post('entry/password/check/','entry_password_check');
|
Route::post('entry/password/check/','entry_password_check');
|
||||||
Route::post('entry/attr/add/{id}','entry_attr_add');
|
|
||||||
Route::post('entry/objectclass/add','entry_objectclass_add');
|
Route::post('entry/objectclass/add','entry_objectclass_add');
|
||||||
Route::post('entry/rename','entry_rename');
|
Route::post('entry/rename','entry_rename');
|
||||||
|
|
||||||
Route::post('entry/update/commit','entry_update');
|
Route::post('entry/update/commit','entry_update');
|
||||||
Route::post('entry/update/pending','entry_pending_update');
|
Route::post('entry/update/pending','entry_pending_update');
|
||||||
|
|
||||||
Route::post('import/process/{type}','import');
|
Route::post('import/process/{type}','import');
|
||||||
|
|
||||||
|
Route::view('modal/copy-move/{dn}','modals.entry-copy-move');
|
||||||
Route::view('modal/delete/{dn}','modals.entry-delete');
|
Route::view('modal/delete/{dn}','modals.entry-delete');
|
||||||
Route::view('modal/export/{dn}','modals.entry-export');
|
Route::view('modal/export/{dn}','modals.entry-export');
|
||||||
Route::view('modal/rename/{dn}','modals.entry-rename');
|
Route::view('modal/rename/{dn}','modals.entry-rename');
|
||||||
@ -69,4 +72,5 @@ Route::controller(AjaxController::class)
|
|||||||
Route::post('children','children');
|
Route::post('children','children');
|
||||||
Route::post('schema/view','schema_view');
|
Route::post('schema/view','schema_view');
|
||||||
Route::post('schema/objectclass/attrs/{id}','schema_objectclass_attrs');
|
Route::post('schema/objectclass/attrs/{id}','schema_objectclass_attrs');
|
||||||
|
Route::post('subordinates','subordinates');
|
||||||
});
|
});
|
Loading…
x
Reference in New Issue
Block a user