Add UI for client API keys

This commit is contained in:
Dane Everitt 2018-02-28 23:30:39 -06:00
parent 2017e640b6
commit 9b93629f45
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
9 changed files with 215 additions and 85 deletions

View file

@ -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.

View 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);
}
}

View 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

View file

@ -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">

View file

@ -18,57 +18,70 @@
@endsection
@section('content')
<div class="row">
<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)
@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> &bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;
</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
&mdash;
@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>
</div>
</div>
</div>
@endsection
@section('footer-scripts')
@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({

View file

@ -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') &rarr;</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>
</form>
</div>
@endsection

View file

@ -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>

View file

@ -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');
});
/*
|--------------------------------------------------------------------------