Add UI for client API keys
This commit is contained in:
parent
2017e640b6
commit
9b93629f45
9 changed files with 215 additions and 85 deletions
|
@ -8,6 +8,9 @@ This project follows [Semantic Versioning](http://semver.org) guidelines.
|
|||
* Fixes a bug when reinstalling a server that would not mark the server as installing, resulting in some UI issues.
|
||||
* Handle 404 errors from missing models in the application API bindings correctly.
|
||||
|
||||
### Added
|
||||
* Adds back client API for sending commands or power toggles to a server though the Panel API: `/api/client/servers/<identifier>`
|
||||
|
||||
## v0.7.3 (Derelict Dermodactylus)
|
||||
### Fixed
|
||||
* Fixes server creation API endpoint not passing the provided `external_id` to the creation service.
|
||||
|
|
109
app/Http/Controllers/Base/ClientApiController.php
Normal file
109
app/Http/Controllers/Base/ClientApiController.php
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Controllers\Base;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Pterodactyl\Models\ApiKey;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Pterodactyl\Http\Controllers\Controller;
|
||||
use Pterodactyl\Services\Api\KeyCreationService;
|
||||
use Pterodactyl\Http\Requests\Base\CreateClientApiKeyRequest;
|
||||
use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface;
|
||||
|
||||
class ClientApiController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var \Prologue\Alerts\AlertsMessageBag
|
||||
*/
|
||||
private $alert;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Services\Api\KeyCreationService
|
||||
*/
|
||||
private $creationService;
|
||||
|
||||
/**
|
||||
* @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface
|
||||
*/
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* ClientApiController constructor.
|
||||
*
|
||||
* @param \Prologue\Alerts\AlertsMessageBag $alert
|
||||
* @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository
|
||||
* @param \Pterodactyl\Services\Api\KeyCreationService $creationService
|
||||
*/
|
||||
public function __construct(AlertsMessageBag $alert, ApiKeyRepositoryInterface $repository, KeyCreationService $creationService)
|
||||
{
|
||||
$this->alert = $alert;
|
||||
$this->creationService = $creationService;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all of the API keys available to this user.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
return view('base.api.index', [
|
||||
'keys' => $this->repository->getAccountKeys($request->user()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render UI to allow creation of an API key.
|
||||
*
|
||||
* @return \Illuminate\View\View
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('base.api.new');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the API key and return the user to the key listing page.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Base\CreateClientApiKeyRequest $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function store(CreateClientApiKeyRequest $request): RedirectResponse
|
||||
{
|
||||
$allowedIps = null;
|
||||
if (! is_null($request->input('allowed_ips'))) {
|
||||
$allowedIps = json_encode(explode(PHP_EOL, $request->input('allowed_ips')));
|
||||
}
|
||||
|
||||
$this->creationService->setKeyType(ApiKey::TYPE_ACCOUNT)->handle([
|
||||
'memo' => $request->input('memo'),
|
||||
'allowed_ips' => $allowedIps,
|
||||
'user_id' => $request->user()->id,
|
||||
]);
|
||||
|
||||
$this->alert->success('A new client API key has been generated for your account.')->flash();
|
||||
|
||||
return redirect()->route('account.api');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a client's API key from the panel.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param $identifier
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function delete(Request $request, $identifier): Response
|
||||
{
|
||||
$this->repository->deleteAccountKey($request->user(), $identifier);
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
}
|
21
app/Http/Requests/Base/CreateClientApiKeyRequest.php
Normal file
21
app/Http/Requests/Base/CreateClientApiKeyRequest.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Base;
|
||||
|
||||
use Pterodactyl\Http\Requests\FrontendUserFormRequest;
|
||||
|
||||
class CreateClientApiKeyRequest extends FrontendUserFormRequest
|
||||
{
|
||||
/**
|
||||
* Validate the data being provided.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'memo' => 'required|string|max:255',
|
||||
'allowed_ips' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -15,7 +15,7 @@
|
|||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<form method="POST" action="{{ route('admin.api.new') }}">
|
||||
<form method="POST" action="{{ route('account.api.new') }}">
|
||||
<div class="col-sm-8 col-xs-12">
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
|
|
|
@ -20,43 +20,48 @@
|
|||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header">
|
||||
<h3 class="box-title">@lang('base.api.index.list')</h3>
|
||||
<div class="box box-primary">
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Credentials List</h3>
|
||||
<div class="box-tools">
|
||||
<a href="{{ route('account.api.new') }}"><button class="btn btn-primary btn-sm">Create New</button></a>
|
||||
<a href="{{ route('account.api.new') }}" class="btn btn-sm btn-primary">Create New</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-body table-responsive no-padding">
|
||||
<table class="table table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>@lang('strings.memo')</th>
|
||||
<th>@lang('strings.public_key')</th>
|
||||
<th class="text-right hidden-sm hidden-xs">@lang('strings.last_used')</th>
|
||||
<th class="text-right hidden-sm hidden-xs">@lang('strings.created')</th>
|
||||
<th>Key</th>
|
||||
<th>Memo</th>
|
||||
<th>Last Used</th>
|
||||
<th>Created</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@foreach($keys as $key)
|
||||
<tr>
|
||||
<td>
|
||||
<code class="toggle-display" style="cursor:pointer" data-toggle="tooltip" data-placement="right" title="Click to Reveal">
|
||||
<i class="fa fa-key"></i> ••••••••
|
||||
</code>
|
||||
<code class="hidden" data-attr="api-key">
|
||||
{{ $key->identifier }}{{ decrypt($key->token) }}
|
||||
</code>
|
||||
</td>
|
||||
<td>{{ $key->memo }}</td>
|
||||
<td><code>{{ $key->identifier . decrypt($key->token) }}</code></td>
|
||||
<td class="text-right hidden-sm hidden-xs">
|
||||
<td>
|
||||
@if(!is_null($key->last_used_at))
|
||||
@datetimeHuman($key->last_used_at)
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-right hidden-sm hidden-xs">
|
||||
@datetimeHuman($key->created_at)
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="#delete" class="text-danger" data-action="delete" data-attr="{{ $key->identifier }}"><i class="fa fa-trash"></i></a>
|
||||
<td>@datetimeHuman($key->created_at)</td>
|
||||
<td>
|
||||
<a href="#" data-action="revoke-key" data-attr="{{ $key->identifier }}">
|
||||
<i class="fa fa-trash-o text-danger"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -68,7 +73,15 @@
|
|||
@parent
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('[data-action="delete"]').click(function (event) {
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
});
|
||||
$('.toggle-display').on('click', function () {
|
||||
$(this).parent().find('code[data-attr="api-key"]').removeClass('hidden');
|
||||
$(this).hide();
|
||||
});
|
||||
|
||||
$('[data-action="revoke-key"]').click(function (event) {
|
||||
var self = $(this);
|
||||
event.preventDefault();
|
||||
swal({
|
||||
|
|
|
@ -8,55 +8,40 @@
|
|||
<h1>@lang('base.api.new.header')<small>@lang('base.api.new.header_sub')</small></h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ route('index') }}">@lang('strings.home')</a></li>
|
||||
<li><a href="{{ route('account.api') }}">@lang('navigation.account.api_access')</a></li>
|
||||
<li class="active">@lang('strings.new')</li>
|
||||
<li class="active">@lang('navigation.account.api_access')</li>
|
||||
<li class="active">@lang('base.api.new.header')</li>
|
||||
</ol>
|
||||
@endsection
|
||||
|
||||
@section('footer-scripts')
|
||||
@parent
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
$('#selectAllCheckboxes').on('click', function () {
|
||||
$('input[type=checkbox]').prop('checked', true);
|
||||
});
|
||||
$('#unselectAllCheckboxes').on('click', function () {
|
||||
$('input[type=checkbox]').prop('checked', false);
|
||||
});
|
||||
})
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<form action="{{ route('account.api.new') }}" method="POST">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-header with-border">
|
||||
<div class="box-title">@lang('base.api.new.form_title')</div>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('account.api.new') }}">
|
||||
<div class="col-sm-6 col-xs-12">
|
||||
<div class="box box-primary">
|
||||
<div class="box-body">
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-12 col-lg-6">
|
||||
<label>@lang('base.api.new.descriptive_memo.title')</label>
|
||||
<input type="text" name="memo" class="form-control" name />
|
||||
<p class="help-block">@lang('base.api.new.descriptive_memo.description')</p>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="memoField">Description <span class="field-required"></span></label>
|
||||
<input id="memoField" type="text" name="memo" class="form-control" value="{{ old('memo') }}">
|
||||
</div>
|
||||
<div class="form-group col-xs-12 col-lg-6">
|
||||
<label>@lang('base.api.new.allowed_ips.title')</label>
|
||||
<textarea name="allowed_ips" class="form-control" name></textarea>
|
||||
<p class="help-block">@lang('base.api.new.allowed_ips.description')</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{!! csrf_field() !!}
|
||||
<button class="btn btn-success pull-right">@lang('strings.create') →</button>
|
||||
<p class="text-muted">Set an easy to understand description for this API key to help you identify it later on.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xs-12">
|
||||
<div class="box box-primary">
|
||||
<div class="box-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="allowedIps">Allowed Connection IPs <span class="field-optional"></span></label>
|
||||
<textarea id="allowedIps" name="allowed_ips" class="form-control" rows="5">{{ old('allowed_ips') }}</textarea>
|
||||
</div>
|
||||
<p class="text-muted">If you would like to limit this API key to specific IP addresses enter them above, one per line. CIDR notation is allowed for each IP address. Leave blank to allow any IP address.</p>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-success btn-sm pull-right">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
@ -101,11 +101,11 @@
|
|||
<i class="fa fa-lock"></i> <span>@lang('navigation.account.security_controls')</span>
|
||||
</a>
|
||||
</li>
|
||||
{{--<li class="{{ (Route::currentRouteName() !== 'account.api' && Route::currentRouteName() !== 'account.api.new') ?: 'active' }}">--}}
|
||||
{{--<a href="{{ route('account.api')}}">--}}
|
||||
{{--<i class="fa fa-code"></i> <span>@lang('navigation.account.api_access')</span>--}}
|
||||
{{--</a>--}}
|
||||
{{--</li>--}}
|
||||
<li class="{{ (Route::currentRouteName() !== 'account.api' && Route::currentRouteName() !== 'account.api.new') ?: 'active' }}">
|
||||
<a href="{{ route('account.api')}}">
|
||||
<i class="fa fa-code"></i> <span>@lang('navigation.account.api_access')</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{ Route::currentRouteName() !== 'index' ?: 'active' }}">
|
||||
<a href="{{ route('index')}}">
|
||||
<i class="fa fa-server"></i> <span>@lang('navigation.account.my_servers')</span>
|
||||
|
|
|
@ -30,16 +30,15 @@ Route::group(['prefix' => 'account'], function () {
|
|||
|
|
||||
| Endpoint: /account/api
|
||||
|
|
||||
| Temporarily Disabled
|
||||
*/
|
||||
//Route::group(['prefix' => 'account/api'], function () {
|
||||
// Route::get('/', 'AccountKeyController@index')->name('account.api');
|
||||
// Route::get('/new', 'AccountKeyController@create')->name('account.api.new');
|
||||
//
|
||||
// Route::post('/new', 'AccountKeyController@store');
|
||||
//
|
||||
// Route::delete('/revoke/{identifier}', 'AccountKeyController@revoke')->name('account.api.revoke');
|
||||
//});
|
||||
Route::group(['prefix' => 'account/api'], function () {
|
||||
Route::get('/', 'ClientApiController@index')->name('account.api');
|
||||
Route::get('/new', 'ClientApiController@create')->name('account.api.new');
|
||||
|
||||
Route::post('/new', 'ClientApiController@store');
|
||||
|
||||
Route::delete('/revoke/{identifier}', 'ClientApiController@delete')->name('account.api.revoke');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
Loading…
Reference in a new issue