Complete new service, option, and variable management interface in Admin CP

This commit is contained in:
Dane Everitt 2017-03-12 00:00:06 -05:00
parent bccbb309b2
commit d7682bb7c9
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
16 changed files with 698 additions and 925 deletions

View File

@ -27,15 +27,63 @@ namespace Pterodactyl\Http\Controllers\Admin;
use Log;
use Alert;
use Storage;
use Pterodactyl\Models;
use Javascript;
use Illuminate\Http\Request;
use Pterodactyl\Models\Service;
use Pterodactyl\Models\ServiceOption;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\OptionRepository;
use Pterodactyl\Repositories\VariableRepository;
use Pterodactyl\Exceptions\DisplayValidationException;
class OptionController extends Controller
{
/**
* Handles request to view page for adding new option.
*
* @param Request $request
* @return \Illuminate\View\View
*/
public function new(Request $request)
{
$services = Service::with('options')->get();
Javascript::put(['services' => $services->keyBy('id')]);
return view('admin.services.options.new', ['services' => $services]);
}
/**
* Handles POST request to create a new option.
* @param Request $request
* @return \Illuminate\Response\RedirectResponse
*/
public function create(Request $request)
{
$repo = new OptionRepository;
try {
$option = $repo->create($request->intersect([
'service_id', 'name', 'description', 'tag',
'docker_image', 'startup', 'config_from', 'config_startup',
'config_logs', 'config_files', 'config_stop'
]));
Alert::success('Successfully created new service option.')->flash();
return redirect()->route('admin.services.option.view', $option->id);
} catch (DisplayValidationException $ex) {
return redirect()->route('admin.services.option.new')->withErrors(json_decode($ex->getMessage()))->withInput();
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unhandled exception occurred while attempting to create this service. This error has been logged.')->flash();
}
return redirect()->route('admin.services.option.new')->withInput();
}
/**
* Display option overview page.
*
@ -45,27 +93,89 @@ class OptionController extends Controller
*/
public function viewConfiguration(Request $request, $id)
{
return view('admin.services.options.view', ['option' => Models\ServiceOption::findOrFail($id)]);
return view('admin.services.options.view', ['option' => ServiceOption::findOrFail($id)]);
}
/**
* Display variable overview page for a service option.
*
* @param Request $request
* @param int $id
* @return \Illuminate\View\View
*/
public function viewVariables(Request $request, $id)
{
return view('admin.services.options.variables', ['option' => ServiceOption::with('variables')->findOrFail($id)]);
}
/**
* Handles POST when editing a configration for a service option.
*
* @param Request $request
* @return \Illuminate\Response\RedirectResponse
*/
public function editConfiguration(Request $request, $id)
{
$repo = new OptionRepository;
try {
$repo->update($id, $request->intersect([
'name', 'description', 'tag', 'docker_image', 'startup',
'config_from', 'config_stop', 'config_logs', 'config_files', 'config_startup',
]));
if ($request->input('action') !== 'delete') {
$repo->update($id, $request->intersect([
'name', 'description', 'tag', 'docker_image', 'startup',
'config_from', 'config_stop', 'config_logs', 'config_files', 'config_startup',
]));
Alert::success('Service option configuration has been successfully updated.')->flash();
} else {
$option = ServiceOption::with('service')->where('id', $id)->first();
$repo->delete($id);
Alert::success('Successfully deleted service option from the system.')->flash();
Alert::success('Service option configuration has been successfully updated.')->flash();
return redirect()->route('admin.services.view', $option->service_id);
}
} catch (DisplayValidationException $ex) {
return redirect()->route('admin.services.option.view', $id)->withErrors(json_decode($ex->getMessage()));
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unhandled exception occurred while attempting to update this service option. This error has been logged.')->flash();
Alert::danger('An unhandled exception occurred while attempting to perform that action. This error has been logged.')->flash();
}
return redirect()->route('admin.services.option.view', $id);
}
/**
* Handles POST when editing a configration for a service option.
*
* @param Request $request
* @param int $option
* @param int $variable
* @return \Illuminate\Response\RedirectResponse
*/
public function editVariable(Request $request, $option, $variable)
{
$repo = new VariableRepository;
try {
if ($request->input('action') !== 'delete') {
$variable = $repo->update($variable, $request->only([
'name', 'description', 'env_variable',
'default_value', 'options', 'rules',
]));
Alert::success("The service variable '{$variable->name}' has been updated.")->flash();
} else {
$repo->delete($variable);
Alert::success("That service variable has been deleted.")->flash();
}
} catch (DisplayValidationException $ex) {
return redirect()->route('admin.services.option.variables', $option)->withErrors(json_decode($ex->getMessage()));
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.')->flash();
}
return redirect()->route('admin.services.option.variables', $option);
}
}

View File

@ -103,32 +103,6 @@ class ServiceController extends Controller
return redirect()->route('admin.services.new')->withInput();
}
/**
* Delete a service from the system.
*
* @param Request $request
* @param int $id
* @return \Illuminate\Response\RedirectResponse
*/
public function delete(Request $request, $id)
{
$repo = new ServiceRepository;
try {
$repo->delete($id);
Alert::success('Successfully deleted service.')->flash();
return redirect()->route('admin.services');
} catch (DisplayException $ex) {
Alert::danger($ex->getMessage())->flash();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An error was encountered while attempting to delete that service. This error has been logged')->flash();
}
return redirect()->route('admin.services.view', $id);
}
/**
* Edits configuration for a specific service.
*
@ -141,10 +115,17 @@ class ServiceController extends Controller
$repo = new ServiceRepository;
try {
$repo->update($id, $request->intersect([
'name', 'description', 'folder', 'startup',
]));
Alert::success('Service has been updated successfully.')->flash();
if ($request->input('action') !== 'delete') {
$repo->update($id, $request->intersect([
'name', 'description', 'folder', 'startup',
]));
Alert::success('Service has been updated successfully.')->flash();
} else {
$repo->delete($id);
Alert::success('Successfully deleted service from the system.')->flash();
return redirect()->route('admin.services');
}
} catch (DisplayValidationException $ex) {
return redirect()->route('admin.services.view', $id)->withErrors(json_decode($ex->getMessage()))->withInput();
} catch (DisplayException $ex) {

View File

@ -419,20 +419,25 @@ class AdminRoutes
'uses' => 'Admin\OptionController@new',
]);
$router->post('/option/new', 'Admin\OptionController@create');
$router->get('/option/{id}', [
'as' => 'admin.services.option.view',
'uses' => 'Admin\OptionController@viewConfiguration',
]);
$router->get('/option/{id}/variables', [
'as' => 'admin.services.option.view.variables',
'as' => 'admin.services.option.variables',
'uses' => 'Admin\OptionController@viewVariables',
]);
$router->post('/option/{id}', [
'uses' => 'Admin\OptionController@editConfiguration',
$router->post('/option/{id}/variables/{variable}', [
'as' => 'admin.services.option.variables.edit',
'uses' => 'Admin\OptionController@editVariable',
]);
$router->post('/option/{id}', 'Admin\OptionController@editConfiguration');
});
// Service Packs

View File

@ -41,7 +41,7 @@ class Service extends Model
* @var array
*/
protected $fillable = [
'name', 'description', 'file', 'executable', 'startup',
'name', 'description', 'folder', 'startup',
];
/**

View File

@ -28,10 +28,74 @@ use DB;
use Validator;
use Pterodactyl\Models\ServiceOption;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Repositories\VariableRepository;
use Pterodactyl\Exceptions\DisplayValidationException;
class OptionRepository
{
/**
* Creates a new service option on the system.
*
* @param array $data
* @return \Pterodactyl\Models\ServiceOption
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\DisplayValidationException
*/
public function create(array $data)
{
$validator = Validator::make($data, [
'service_id' => 'required|numeric|exists:services,id',
'name' => 'required|string|max:255',
'description' => 'required|string',
'tag' => 'required|string|max:255|unique:service_options,tag',
'docker_image' => 'required|string|max:255',
'startup' => 'required|string',
'config_from' => 'sometimes|required|numeric|exists:service_options,id',
'config_startup' => 'required_without:config_from|json',
'config_stop' => 'required_without:config_from|string|max:255',
'config_logs' => 'required_without:config_from|json',
'config_files' => 'required_without:config_from|json',
]);
if ($validator->fails()) {
throw new DisplayValidationException($validator->errors());
}
if (isset($data['config_from'])) {
if (! ServiceOption::where('service_id', $data['service_id'])->where('id', $data['config_from'])->first()) {
throw new DisplayException('The `configuration from` directive must be a child of the assigned service.');
}
}
return ServiceOption::create($data);
}
/**
* Deletes a service option from the system.
*
* @param int $id
* @return void
*
* @throws \Pterodactyl\Exceptions\DisplayException
*/
public function delete($id)
{
$option = ServiceOption::with('variables')->withCount('servers')->findOrFail($id);
if ($option->servers_count > 0) {
throw new DisplayException('You cannot delete a service option that has servers associated with it.');
}
DB::transaction(function () use ($option) {
foreach($option->variables as $variable) {
(new VariableRepository)->delete($variable->id);
}
$option->delete();
});
}
/**
* Updates a service option in the database which can then be used
* on nodes.
@ -39,11 +103,25 @@ class OptionRepository
* @param int $id
* @param array $data
* @return \Pterodactyl\Models\ServiceOption
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\DisplayValidationException
*/
public function update($id, array $data)
{
$option = ServiceOption::findOrFail($id);
// Due to code limitations (at least when I am writing this currently)
// we have to make an assumption that if config_from is not passed
// that we should be telling it that no config is wanted anymore.
//
// This really is only an issue if we open API access to this function,
// in which case users will always need to pass `config_from` in order
// to keep it assigned.
if (! isset($data['config_from']) && ! is_null($option->config_from)) {
$option->config_from = null;
}
$validator = Validator::make($data, [
'name' => 'sometimes|required|string|max:255',
'description' => 'sometimes|required|string',
@ -73,6 +151,12 @@ class OptionRepository
throw new DisplayValidationException($validator->errors());
}
if (isset($data['config_from'])) {
if (! ServiceOption::where('service_id', $option->service_id)->where('id', $data['config_from'])->first()) {
throw new DisplayException('The `configuration from` directive must be a child of the assigned service.');
}
}
$option->fill($data)->save();
return $option;

View File

@ -31,6 +31,7 @@ use Validator;
use Pterodactyl\Models\Service;
use Pterodactyl\Models\ServiceVariable;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Repositories\OptionRepository;
use Pterodactyl\Exceptions\DisplayValidationException;
class ServiceRepository
@ -55,13 +56,14 @@ class ServiceRepository
}
$service = DB::transaction(function () use ($data) {
$service = Service::create([
'author' => config('pterodactyl.service.author'),
$service = new Service;
$service->author = config('pterodactyl.service.author');
$service->fill([
'name' => $data['name'],
'description' => (isset($data['description'])) ? $data['description'] : null,
'folder' => $data['folder'],
'startup' => (isset($data['startup'])) ? $data['startup'] : null,
]);
])->save();
// It is possible for an event to return false or throw an exception
// which won't necessarily be detected by this transaction.
@ -105,8 +107,7 @@ class ServiceRepository
$moveFiles = (isset($data['folder']) && $data['folder'] !== $service->folder);
$oldFolder = $service->folder;
$service->fill($data);
$service->save();
$service->fill($data)->save();
if ($moveFiles) {
Storage::move(sprintf('services/%s/index.js', $oldFolder), sprintf('services/%s/index.js', $service->folder));
@ -124,18 +125,16 @@ class ServiceRepository
*/
public function delete($id)
{
$service = Service::withCount('servers', 'options')->findOrFail($id);
$service = Service::withCount('servers')->with('options')->findOrFail($id);
if ($service->servers_count > 0) {
throw new DisplayException('You cannot delete a service that has servers associated with it.');
}
DB::transaction(function () use ($service) {
ServiceVariable::whereIn('option_id', $service->options->pluck('id')->all())->delete();
$service->options->each(function ($item) {
$item->delete();
});
foreach($service->options as $option) {
(new OptionRepository)->delete($option->id);
}
$service->delete();
Storage::deleteDirectory('services/' . $service->folder);

View File

@ -26,7 +26,7 @@ namespace Pterodactyl\Repositories;
use DB;
use Validator;
use Pterodactyl\Models;
use Pterodactyl\Models\ServiceVariable;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Exceptions\DisplayValidationException;
@ -37,108 +37,126 @@ class VariableRepository
//
}
public function create($id, array $data)
public function create(array $data)
{
$option = Models\ServiceOption::select('id')->findOrFail($id);
$validator = Validator::make($data, [
'option_id' => 'required|numeric|exists:service_options,id',
'name' => 'required|string|min:1|max:255',
'description' => 'required|string',
'description' => 'sometimes|nullable|string',
'env_variable' => 'required|regex:/^[\w]{1,255}$/',
'default_value' => 'string|max:255',
'user_viewable' => 'sometimes|required|nullable|boolean',
'user_editable' => 'sometimes|required|nullable|boolean',
'required' => 'sometimes|required|nullable|boolean',
'regex' => 'required|string|min:1',
'default_value' => 'string',
'options' => 'sometimes|required|array',
'rules' => 'bail|required|string|min:1',
]);
// Ensure the default value is allowed by the rules provided.
$rules = (isset($data['rules'])) ? $data['rules'] : $variable->rules;
$validator->sometimes('default_value', $rules, function ($input) {
return $input->default_value;
});
if ($validator->fails()) {
throw new DisplayValidationException($validator->errors());
}
if ($data['default_value'] !== '' && ! preg_match($data['regex'], $data['default_value'])) {
throw new DisplayException('The default value you entered cannot violate the regex requirements.');
if (isset($data['env_variable'])) {
$search = ServiceVariable::where('env_variable', $data['env_variable'])
->where('option_id', $variable->option_id)
->where('id', '!=', $variable->id);
if ($search->first()) {
throw new DisplayException('The envionment variable name assigned to this variable must be unique for this service option.');
}
}
if (Models\ServiceVariable::where('env_variable', $data['env_variable'])->where('option_id', $option->id)->first()) {
throw new DisplayException('An environment variable with that name already exists for this option.');
if (! isset($data['options']) || ! is_array($data['options'])) {
$data['options'] = [];
}
$data['user_viewable'] = (isset($data['user_viewable']) && in_array((int) $data['user_viewable'], [0, 1])) ? $data['user_viewable'] : 0;
$data['user_editable'] = (isset($data['user_editable']) && in_array((int) $data['user_editable'], [0, 1])) ? $data['user_editable'] : 0;
$data['required'] = (isset($data['required']) && in_array((int) $data['required'], [0, 1])) ? $data['required'] : 0;
$data['option_id'] = $option->id;
$data['user_viewable'] = (in_array('user_viewable', $data['options']));
$data['user_editable'] = (in_array('user_editable', $data['options']));
$data['required'] = (in_array('required', $data['options']));
$variable = Models\ServiceVariable::create($data);
// Remove field that isn't used.
unset($data['options']);
return $variable;
return ServiceVariable::create($data);
}
/**
* Deletes a specified option variable as well as all server
* variables currently assigned.
*
* @param int $id
* @return void
*/
public function delete($id)
{
$variable = Models\ServiceVariable::with('serverVariable')->findOrFail($id);
$variable = ServiceVariable::with('serverVariable')->findOrFail($id);
DB::beginTransaction();
try {
foreach ($variable->serverVariable as $svar) {
$svar->delete();
DB::transaction(function () use ($variable) {
foreach ($variable->serverVariable as $v) {
$v->delete();
}
$variable->delete();
DB::commit();
} catch (\Exception $ex) {
DB::rollBack();
throw $ex;
}
$variable->delete();
});
}
/**
* Updates a given service variable.
*
* @param int $id
* @param array $data
* @return \Pterodactyl\Models\ServiceVariable
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\DisplayValidationException
*/
public function update($id, array $data)
{
$variable = Models\ServiceVariable::findOrFail($id);
$variable = ServiceVariable::findOrFail($id);
$validator = Validator::make($data, [
'name' => 'sometimes|required|string|min:1|max:255',
'description' => 'sometimes|required|string',
'description' => 'sometimes|nullable|string',
'env_variable' => 'sometimes|required|regex:/^[\w]{1,255}$/',
'default_value' => 'sometimes|string|max:255',
'user_viewable' => 'sometimes|required|nullable|boolean',
'user_editable' => 'sometimes|required|nullable|boolean',
'required' => 'sometimes|required|nullable|boolean',
'regex' => 'sometimes|required|string|min:1',
'default_value' => 'string',
'options' => 'sometimes|required|array',
'rules' => 'bail|sometimes|required|string|min:1',
]);
// Ensure the default value is allowed by the rules provided.
$rules = (isset($data['rules'])) ? $data['rules'] : $variable->rules;
$validator->sometimes('default_value', $rules, function ($input) {
return $input->default_value;
});
if ($validator->fails()) {
throw new DisplayValidationException($validator->errors());
}
$data['default_value'] = (isset($data['default_value'])) ? $data['default_value'] : $variable->default_value;
$data['regex'] = (isset($data['regex'])) ? $data['regex'] : $variable->regex;
if ($data['default_value'] !== '' && ! preg_match($data['regex'], $data['default_value'])) {
throw new DisplayException('The default value you entered cannot violate the regex requirements.');
if (isset($data['env_variable'])) {
$search = ServiceVariable::where('env_variable', $data['env_variable'])
->where('option_id', $variable->option_id)
->where('id', '!=', $variable->id);
if ($search->first()) {
throw new DisplayException('The envionment variable name assigned to this variable must be unique for this service option.');
}
}
if (Models\ServiceVariable::where('id', '!=', $variable->id)->where('env_variable', $data['env_variable'])->where('option_id', $variable->option_id)->first()) {
throw new DisplayException('An environment variable with that name already exists for this option.');
if (! isset($data['options']) || ! is_array($data['options'])) {
$data['options'] = [];
}
$data['user_viewable'] = (isset($data['user_viewable']) && in_array((int) $data['user_viewable'], [0, 1])) ? $data['user_viewable'] : $variable->user_viewable;
$data['user_editable'] = (isset($data['user_editable']) && in_array((int) $data['user_editable'], [0, 1])) ? $data['user_editable'] : $variable->user_editable;
$data['required'] = (isset($data['required']) && in_array((int) $data['required'], [0, 1])) ? $data['required'] : $variable->required;
$data['user_viewable'] = (in_array('user_viewable', $data['options']));
$data['user_editable'] = (in_array('user_editable', $data['options']));
$data['required'] = (in_array('required', $data['options']));
// Not using $data because the function that passes into this function
// can't do $requst->only() due to the page setup.
$variable->fill([
'name' => $data['name'],
'description' => $data['description'],
'env_variable' => $data['env_variable'],
'default_value' => $data['default_value'],
'user_viewable' => $data['user_viewable'],
'user_editable' => $data['user_editable'],
'required' => $data['required'],
'regex' => $data['regex'],
]);
// Remove field that isn't used.
unset($data['options']);
return $variable->save();
$variable->fill($data)->save();
return $variable;
}
}

View File

@ -12,6 +12,7 @@ return [
| standard Pterodactyl shipped services.
*/
'service' => [
'core' => 'ptrdctyl-v040-11e6-8b77-86f30ca893d3',
'author' => env('SERVICE_AUTHOR'),
],

View File

@ -0,0 +1,47 @@
<?php
use Illuminate\Support\Facades\Schema;
use Pterodactyl\Models\ServiceVariable;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class ChangeServiceVariablesValidationRules extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('service_variables', function (Blueprint $table) {
$table->renameColumn('regex', 'rules');
});
DB::transaction(function () {
foreach(ServiceVariable::all() as $variable) {
$variable->rules = ($variable->required) ? 'required|regex:' . $variable->rules : 'regex:' . $variable->regex;
$variable->save();
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('service_variables', function (Blueprint $table) {
$table->renameColumn('rules', 'regex');
});
DB::transaction(function () {
foreach(ServiceVariable::all() as $variable) {
$variable->regex = str_replace(['required|regex:', 'regex:'], '', $variable->regex);
$variable->save();
}
});
}
}

View File

@ -0,0 +1,170 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}}
{{-- of this software and associated documentation files (the "Software"), to deal --}}
{{-- in the Software without restriction, including without limitation the rights --}}
{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}}
{{-- copies of the Software, and to permit persons to whom the Software is --}}
{{-- furnished to do so, subject to the following conditions: --}}
{{-- The above copyright notice and this permission notice shall be included in all --}}
{{-- copies or substantial portions of the Software. --}}
{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}}
{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}}
{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}}
{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}}
{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}}
{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}}
{{-- SOFTWARE. --}}
@extends('layouts.admin')
@section('title')
Services &rarr; New Option
@endsection
@section('content-header')
<h1>New Option<small>Create a new service option to assign to servers.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.services') }}">Services</a></li>
<li class="active">New Service Option</li>
</ol>
@endsection
@section('content')
<form action="{{ route('admin.services.option.new') }}" method="POST">
<div class="row">
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Configuration</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label for="pServiceId" class="form-label">Associated Service</label>
<select name="service_id" id="pServiceId">
@foreach($services as $service)
<option value="{{ $service->id }}">{{ $service->name }}</option>
@endforeach
</select>
</div>
<div class="form-group">
<label for="pName" class="form-label">Option Name</label>
<input type="text" id="pName" name="name" value="{{ old('name') }}" class="form-control" />
<p class="text-muted small">A simple, human-readable name to use as an identifier for this service.</p>
</div>
<div class="form-group">
<label for="pDescription" class="form-label">Description</label>
<textarea id="pDescription" name="description" class="form-control" rows="8">{{ old('description') }}</textarea>
<p class="text-muted small">A description of this service that will be displayed throughout the panel as needed.</p>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="pTag" class="form-label">Option Tag</label>
<input type="text" id="pTag" name="tag" value="{{ old('tag') }}" class="form-control" />
<p class="text-muted small">This should be a unique identifer for this service option that is not used for any other service options.</p>
</div>
<div class="form-group">
<label for="pDockerImage" class="form-label">Docker Image</label>
<input type="text" id="pDockerImage" name="docker_image" value="{{ old('docker_image') }}" placeholder="quay.io/pterodactyl/service" class="form-control" />
<p class="text-muted small">The default docker image that should be used for new servers under this service option. This can be left blank to use the parent service's defined image, and can also be changed per-server.</p>
</div>
<div class="form-group">
<label for="pStartup" class="form-label">Startup Command</label>
<textarea id="pStartup" name="startup" class="form-control" rows="4">{{ old('startup') }}</textarea>
<p class="text-muted small">The default statup command that should be used for new servers under this service option. This can be left blank to use the parent service's startup, and can also be changed per-server.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Process Management</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-xs-12">
<div class="alert alert-warning">
<p>All fields are required unless you select a seperate option from the 'Copy Settings From' dropdown, in which case fields may be left blank to use the values from that option.</p>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="pConfigFrom" class="form-label">Copy Settings From</label>
<select name="config_from" id="pConfigFrom" class="form-control">
<option value="0">None</option>
</select>
<p class="text-muted small">If you would like to default to settings from another option select the option from the menu above.</p>
</div>
<div class="form-group">
<label for="pConfigStop" class="form-label">Stop Command</label>
<input type="text" id="pConfigStop" name="config_stop" class="form-control" value="{{ old('config_stop') }}" />
<p class="text-muted small">The command that should be sent to server processes to stop them gracefully. If you need to send a <code>SIGINT</code> you should enter <code>^C</code> here.</p>
</div>
<div class="form-group">
<label for="pConfigLogs" class="form-label">Log Configuration</label>
<textarea data-action="handle-tabs" id="pConfigLogs" name="config_logs" class="form-control" rows="6">{{ old('config_logs') }}</textarea>
<p class="text-muted small">This should be a JSON representation of where log files are stored, and wether or not the daemon should be creating custom logs.</p>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="pConfigFiles" class="form-label">Configuration Files</label>
<textarea data-action="handle-tabs" id="pConfigFiles" name="config_files" class="form-control" rows="6">{{ old('config_files') }}</textarea>
<p class="text-muted small">This should be a JSON representation of configuration files to modify and what parts should be changed.</p>
</div>
<div class="form-group">
<label for="pConfigStartup" class="form-label">Start Configuration</label>
<textarea data-action="handle-tabs" id="pConfigStartup" name="config_startup" class="form-control" rows="6">{{ old('config_startup') }}</textarea>
<p class="text-muted small">This should be a JSON representation of what values the daemon should be looking for when booting a server to determine completion.</p>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button type="submit" class="btn btn-success btn-sm pull-right">Create Service</button>
</div>
</div>
</div>
</div>
</form>
@endsection
@section('footer-scripts')
@parent
{!! Theme::js('vendor/lodash/lodash.js') !!}
<script>
$(document).ready(function() {
$('#pServiceId').select2().change();
$('#pConfigFrom').select2();
});
$('#pServiceId').on('change', function (event) {
$('#pConfigFrom').html('<option value="0">None</option>').select2({
data: $.map(_.get(Pterodactyl.services, $(this).val() + '.options', []), function (item) {
return {
id: item.id,
text: item.name,
};
}),
});
});
$('textarea[data-action="handle-tabs"]').on('keydown', function(event) {
if (event.keyCode === 9) {
event.preventDefault();
var curPos = $(this)[0].selectionStart;
var prepend = $(this).val().substr(0, curPos);
var append = $(this).val().substr(curPos);
$(this).val(prepend + ' ' + append);
}
});
</script>
@endsection

View File

@ -0,0 +1,114 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}}
{{-- of this software and associated documentation files (the "Software"), to deal --}}
{{-- in the Software without restriction, including without limitation the rights --}}
{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}}
{{-- copies of the Software, and to permit persons to whom the Software is --}}
{{-- furnished to do so, subject to the following conditions: --}}
{{-- The above copyright notice and this permission notice shall be included in all --}}
{{-- copies or substantial portions of the Software. --}}
{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}}
{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}}
{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}}
{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}}
{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}}
{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}}
{{-- SOFTWARE. --}}
@extends('layouts.admin')
@section('title')
Service Options &rarr; {{ $option->name }} &rarr; Variables
@endsection
@section('content-header')
<h1>{{ $option->name }}<small>Managing variables for this service option.</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('admin.index') }}">Admin</a></li>
<li><a href="{{ route('admin.services') }}">Services</a></li>
<li><a href="{{ route('admin.services.view', $option->service->id) }}">{{ $option->service->name }}</a></li>
<li><a href="{{ route('admin.services.option.view', $option->id) }}">{{ $option->name }}</a></li>
<li class="active">Variables</li>
</ol>
@endsection
@section('content')
<div class="row">
<div class="col-xs-12">
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li><a href="{{ route('admin.services.option.view', $option->id) }}">Configuration</a></li>
<li class="active"><a href="{{ route('admin.services.option.variables', $option->id) }}">Variables</a></li>
</ul>
</div>
</div>
</div>
<div class="row">
@foreach($option->variables as $variable)
<div class="col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ $variable->name }}</h3>
</div>
<form action="{{ route('admin.services.option.variables.edit', ['id' => $option->id, 'variable' => $variable->id]) }}" method="POST">
<div class="box-body">
<div class="form-group">
<label class="form-label">Name</label>
<input type="text" name="name" value="{{ $variable->name }}" class="form-control" />
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea name="description" class="form-control" rows="3">{{ $variable->description }}</textarea>
</div>
<div class="row">
<div class="form-group col-md-6">
<label class="form-label">Environment Variable</label>
<input type="text" name="env_variable" value="{{ $variable->env_variable }}" class="form-control" />
</div>
<div class="form-group col-md-6">
<label class="form-label">Default Value</label>
<input type="text" name="default_value" value="{{ $variable->default_value }}" class="form-control" />
</div>
<div class="col-xs-12">
<p class="text-muted small">This variable can be accessed in the statup command by using <code>{{ $variable->env_variable }}</code>.</p>
</div>
</div>
<div class="form-group">
<label class="form-label">Permissions</label>
<select name="options[]" class="pOptions form-control" multiple>
<option value="user_viewable" {{ (! $variable->user_viewable) ?: 'selected' }}>Users Can View</option>
<option value="user_editable" {{ (! $variable->user_editable) ?: 'selected' }}>Users Can Edit</option>
<option value="required" {{ (! $variable->required) ?: 'selected' }}>Field Is Required</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Input Rules</label>
<input type="text" name="rules" class="form-control" value="{{ $variable->rules }}" />
<p class="text-muted small">These rules are defined using standard Laravel Framework validation rules.</p>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button class="btn btn-sm btn-danger pull-left muted muted-hover" data-action="delete" name="action" value="delete" type="submit"><i class="fa fa-trash-o"></i></button>
<button class="btn btn-sm btn-primary pull-right" name="action" value="save" type="submit">Save</button>
</div>
</form>
</div>
</div>
@endforeach
</div>
@endsection
@section('footer-scripts')
@parent
<script>
$('.pOptions').select2();
$('[data-action="delete"]').on('mouseenter', function (event) {
$(this).find('i').html(' Delete Variable');
}).on('mouseleave', function (event) {
$(this).find('i').html('');
});
</script>
@endsection

View File

@ -39,7 +39,7 @@
<div class="nav-tabs-custom nav-tabs-floating">
<ul class="nav nav-tabs">
<li class="active"><a href="{{ route('admin.services.option.view', $option->id) }}">Configuration</a></li>
<li><a href="{{ route('admin.services.option.view.variables', $option->id) }}">Variables</a></li>
<li><a href="{{ route('admin.services.option.variables', $option->id) }}">Variables</a></li>
</ul>
</div>
</div>
@ -131,12 +131,16 @@
<label for="pConfigStartup" class="form-label">Start Configuration</label>
<textarea data-action="handle-tabs" id="pConfigStartup" name="config_startup" class="form-control" rows="6">{{ ! is_null($option->config_startup) ? json_encode(json_decode($option->config_startup), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) : '' }}</textarea>
<p class="text-muted small">This should be a JSON representation of what values the daemon should be looking for when booting a server to determine completion.</p>
</div>
</div>
</div>
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button type="submit" class="btn btn-primary btn-sm pull-right">Edit Service</button>
<button id="deleteButton" type="submit" name="action" value="delete" class="btn btn-danger btn-sm muted muted-hover">
<i class="fa fa-trash-o"></i>
</button>
<button type="submit" name="action" value="edit" class="btn btn-primary btn-sm pull-right">Edit Service</button>
</div>
</div>
</div>
@ -147,6 +151,12 @@
@section('footer-scripts')
@parent
<script>
$('#pConfigFrom').select2();
$('#deleteButton').on('mouseenter', function (event) {
$(this).find('i').html(' Delete Option');
}).on('mouseleave', function (event) {
$(this).find('i').html('');
});
$('textarea[data-action="handle-tabs"]').on('keydown', function(event) {
if (event.keyCode === 9) {
event.preventDefault();

View File

@ -74,6 +74,7 @@
</div>
<div class="box-footer">
{!! csrf_field() !!}
<button id="deleteButton" type="input" name="action" value="delete" class="btn btn-sm btn-danger muted muted-hover"><i class="fa fa-trash-o"></i></button>
<button type="input" class="btn btn-primary btn-sm pull-right">Edit Service</button>
</div>
</div>
@ -111,3 +112,14 @@
</div>
</div>
@endsection
@section('footer-scripts')
@parent
<script>
$('#deleteButton').on('mouseenter', function (event) {
$(this).find('i').html(' Delete Service');
}).on('mouseleave', function (event) {
$(this).find('i').html('');
});
</script>
@endsection

View File

@ -1,687 +0,0 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}}
{{-- of this software and associated documentation files (the "Software"), to deal --}}
{{-- in the Software without restriction, including without limitation the rights --}}
{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}}
{{-- copies of the Software, and to permit persons to whom the Software is --}}
{{-- furnished to do so, subject to the following conditions: --}}
{{-- The above copyright notice and this permission notice shall be included in all --}}
{{-- copies or substantial portions of the Software. --}}
{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}}
{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}}
{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}}
{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}}
{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}}
{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}}
{{-- SOFTWARE. --}}
@extends('layouts.admin')
@section('title')
Managing Server: {{ $server->name }} ({{ $server->uuidShort}})
@endsection
@section('content')
<div class="col-md-12">
<ul class="breadcrumb">
<li><a href="/admin">Admin Control</a></li>
<li><a href="/admin/servers">Servers</a></li>
<li class="active">{{ $server->name }} ({{ $server->uuidShort}})</li>
</ul>
@if($server->suspended === 1 && !$server->trashed())
<div class="alert alert-warning">
This server is suspended and has no user access. Processes cannot be started and files cannot be modified. All API access is disabled unless using a master token.
</div>
@elseif($server->trashed())
<div class="alert alert-danger">
This server is marked for deletion <strong>{{ Carbon::parse($server->deleted_at)->addMinutes(env('APP_DELETE_MINUTES', 10))->diffForHumans() }}</strong>. If you want to cancel this action simply click the button below.
<br /><br />
<form action="{{ route('admin.servers.post.queuedDeletion', $server->id) }}" method="POST">
<button class="btn btn-sm btn-default" name="cancel" value="1">Cancel Deletion</button>
<button class="btn btn-sm btn-danger pull-right" name="force_delete" value="1"><strong>Force</strong> Delete</button>
<button class="btn btn-sm btn-danger pull-right" name="delete" style="margin-right:10px;" value="1">Delete</button>
{!! csrf_field() !!}
</form>
</div>
@endif
@if($server->installed === 0)
<div class="alert alert-warning">
This server is still running through the install process and is not avaliable for use just yet. This message will disappear once this process is completed.
</div>
@elseif($server->installed === 2)
<div class="alert alert-danger">
This server <strong>failed</strong> to install properly. You should delete it and try to create it again or check the daemon logs.
</div>
@endif
<ul class="nav nav-tabs tabs_with_panel" id="config_tabs">
<li class="active"><a href="#tab_about" data-toggle="tab">About</a></li>
@if($server->installed === 1)
<li><a href="#tab_details" data-toggle="tab">Details</a></li>
<li><a href="#tab_build" data-toggle="tab">Build Configuration</a></li>
<li><a href="#tab_startup" data-toggle="tab">Startup</a></li>
<li><a href="#tab_database" data-toggle="tab">Database</a></li>
@endif
@if($server->installed !== 2)
<li><a href="#tab_manage" data-toggle="tab">Manage</a></li>
@endif
@if(!$server->trashed())<li><a href="#tab_delete" data-toggle="tab">Delete</a></li>@endif
</ul>
<div class="tab-content">
<div class="tab-pane active" id="tab_about">
<div class="panel panel-default">
<div class="panel-heading"></div>
<div class="panel-body">
<table class="table table-striped" style="margin-bottom: 0;">
<tbody>
<tr>
<td>UUID</td>
<td>{{ $server->uuid }}</td>
</tr>
<tr>
<td>Docker Container ID</td>
<td data-attr="container-id"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
</tr>
<tr>
<td>Docker User ID</td>
<td data-attr="container-user"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
</tr>
<tr>
<td>Owner</td>
<td><a href="{{ route('admin.users.view', $server->owner_id) }}">{{ $server->user->email }}</a></td>
</tr>
<tr>
<td>Location</td>
<td><a href="{{ route('admin.locations') }}">{{ $server->node->location->short }}</a></td>
</tr>
<tr>
<td>Node</td>
<td><a href="{{ route('admin.nodes.view', $server->node_id) }}">{{ $server->node->name }}</a></td>
</tr>
<tr>
<td>Service</td>
<td>{{ $server->option->service->name }} :: {{ $server->option->name }}</td>
</tr>
<tr>
<td>Name</td>
<td>{{ $server->name }}</td>
</tr>
<tr>
<td>Memory</td>
<td><code>{{ $server->memory }}MB</code> / <code data-toggle="tooltip" data-placement="top" title="Swap Space">{{ $server->swap }}MB</code></td>
</tr>
<tr>
<td><abbr title="Out of Memory">OOM</abbr> Killer</td>
<td>{!! ($server->oom_disabled === 0) ? '<span class="label label-success">Enabled</span>' : '<span class="label label-default">Disabled</span>' !!}</td>
</tr>
<tr>
<td>Disk Space</td>
<td><code>{{ $server->disk }}MB</code></td>
</tr>
<tr>
<td>Block IO Weight</td>
<td><code>{{ $server->io }}</code></td>
</tr>
<tr>
<td>CPU Limit</td>
<td><code>{{ $server->cpu }}%</code></td>
</tr>
<tr>
<td>Default Connection</td>
<td><code>{{ $server->allocation->ip }}:{{ $server->allocation->port }}</code></td>
</tr>
<tr>
<td>Connection Alias</td>
<td>
@if($server->allocation->alias !== $server->allocation->ip)
<code>{{ $server->allocation->alias }}:{{ $server->allocation->port }}</code>
@else
<span class="label label-default">No Alias Assigned</span>
@endif
</td>
</tr>
<tr>
<td>Installed</td>
<td>{!! ($server->installed === 1) ? '<span class="label label-success">Yes</span>' : '<span class="label label-danger">No</span>' !!}</td>
</tr>
<tr>
<td>Suspended</td>
<td>{!! ($server->suspended === 1) ? '<span class="label label-warning">Suspended</span>' : '<span class="label label-success">No</span>' !!}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
@if($server->installed === 1)
<div class="tab-pane" id="tab_details">
<div class="panel panel-default">
<div class="panel-heading"></div>
<div class="panel-body">
<form method="POST" action="/admin/servers/view/{{ $server->id }}/details">
<div class="form-group {{ $errors->has('name') ? 'has-error' : '' }}">
<label for="name" class="control-label">Server Name</label>
<div>
<input type="text" name="name" value="{{ old('name', $server->name) }}" class="form-control" />
<p class="text-muted"><small>Character limits: <code>a-zA-Z0-9_-</code> and <code>[Space]</code> (max 35 characters).</small></p>
</div>
</div>
<div class="form-group {{ $errors->has('owner') ? 'has-error' : '' }}">
<label for="name" class="control-label">Server Owner</label>
<div>
<input type="text" name="owner" value="{{ old('owner', $server->user->email) }}" class="form-control" />
<p class="text-muted"><small>You can change the owner of this server by changing this field to an email matching another use on this system. If you do this a new daemon security token will be generated automatically.</small></p>
</div>
</div>
<div class="form-group">
<label for="name" class="control-label">Daemon Secret Token</label>
<div>
<input type="text" disabled value="{{ $server->daemonSecret }}" class="form-control" />
<p class="text-muted"><small>This token should not be shared with anyone as it has full control over this server.</small></p>
</div>
</div>
<div class="form-group">
<div>
<input type="checkbox" name="reset_token"/> Yes, Reset Daemon Token
<p class="text-muted"><small>Resetting this token will cause any requests using the old token to fail.</small></p>
</div>
</div>
<div class="form-group">
{!! csrf_field() !!}
<input type="submit" class="btn btn-sm btn-primary" value="Update Details" />
</div>
</form>
</div>
<div class="panel-heading" style="border-top: 1px solid #ddd;"></div>
<div class="panel-body">
<form action="{{ route('admin.servers.post.container', $server->id) }}" method="POST">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="name" class="control-label">Docker Container Image</label>
<div>
<input type="text" name="docker_image" value="{{ $server->image }}" class="form-control" />
<p class="text-muted"><small>The docker image to use for this server. The default image for this service and option combination is <code>{{ $server->docker_image }}</code>.</small></p>
</div>
</div>
</div>
<div class="col-md-6 text-center">
<div class="form-group">
<label for="name" class="control-label">&nbsp;</label>
<div>
{!! csrf_field() !!}
<input type="submit" class="btn btn-sm btn-primary" value="Update Docker Image" />
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="tab-pane" id="tab_build">
<div class="panel panel-default">
<div class="panel-heading"></div>
<div class="panel-body">
<div class="row">
<div class="col-md-12">
<div class="alert alert-info">
After editing any of the options below you will need to restart the server for changes to take effect. If the server is currently off, you just need to start it and the container will be rebuilt with the new settings.
</div>
</div>
</div>
<form action="/admin/servers/view/{{ $server->id }}/build" method="POST">
<div class="row">
<div class="col-md-6 form-group {{ $errors->has('memory') ? 'has-error' : '' }}">
<label for="memory" class="control-label">Allocated Memory</label>
<div class="input-group">
<input type="text" name="memory" data-multiplicator="true" class="form-control" value="{{ old('memory', $server->memory) }}"/>
<span class="input-group-addon">MB</span>
</div>
</div>
<div class="col-md-6 form-group {{ $errors->has('swap') ? 'has-error' : '' }}">
<label for="swap" class="control-label">Allocated Swap</label>
<div class="input-group">
<input type="text" name="swap" data-multiplicator="true" class="form-control" value="{{ old('swap', $server->swap) }}"/>
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted"><small>Setting this to <code>0</code> will disable swap space on this server.</small></p>
</div>
</div>
<div class="row">
<div class="col-md-6 form-group {{ $errors->has('cpu') ? 'has-error' : '' }}">
<label for="cpu" class="control-label">CPU Limit</label>
<div class="input-group">
<input type="text" name="cpu" class="form-control" value="{{ old('cpu', $server->cpu) }}"/>
<span class="input-group-addon">%</span>
</div>
<p class="text-muted"><small>Each <em>physical</em> core on the system is considered to be <code>100%</code>. Setting this value to <code>0</code> will allow a server to use CPU time without restrictions.</small></p>
</div>
<div class="col-md-6 form-group {{ $errors->has('io') ? 'has-error' : '' }}">
<label for="io" class="control-label">Block IO Proportion</label>
<div>
<input type="text" name="io" class="form-control" value="{{ old('io', $server->io) }}"/>
</div>
<p class="text-muted"><small>Changing this value can have negative effects on all containers on the system. We strongly recommend leaving this value as <code>500</code>.</small></p>
</div>
</div>
<hr />
<div class="row">
<div class="col-md-12">
<div class="alert alert-info">
Additional IPs and Ports can be assigned to this server for use by plugins or other software. The game port is what will show up for the user to use to connect to thier server, and what their configuration files will be forced to use for binding.
</div>
</div>
<div class="col-md-6 form-group">
<label for="default" class="control-label">Game Port</label>
@foreach ($assigned as $assignment)
<div class="input-group" style="margin:5px auto;">
<span class="input-group-addon">
<input type="radio"
@if($assignment->id === $server->allocation_id) checked="checked" @endif
name="default" value="{{ $assignment->ip }}:{{ $assignment->port }}"/>
</span>
<input type="text" class="form-control" value="{{ $assignment->alias }}:{{ $assignment->port }}"
@if($assignment->has_alias)
data-toggle="tooltip" data-placement="left" title="{{ $assignment->ip }}:{{ $assignment->port }}"
@endif
/>
</div>
@endforeach
</div>
<div class="col-md-6">
<div class="row">
<div class="col-md-12 form-group">
<label for="add_additional" class="control-label">Assign Additional Ports</label>
<div>
<select name="add_additional[]" class="form-control" multiple>
@foreach ($unassigned as $assignment)
<option value="{{ $assignment->ip }}:{{ $assignment->port }}">{{ $assignment->alias }}:{{ $assignment->port }}</option>
@endforeach
</select>
</div>
<p class="text-muted"><small>Please note that due to software limitations you cannot assign identical ports on different IPs to the same server. For example, you <strong>cannot</strong> assign both <code>192.168.0.5:25565</code> and <code>192.168.10.5:25565</code> to the same server.</small></p>
</div>
</div>
<div class="row">
<div class="col-md-12 form-group">
<label for="remove_additional" class="control-label">Remove Additional Ports</label>
<div>
<select name="remove_additional[]" class="form-control" multiple>
@foreach ($assigned as $assignment)
<option value="{{ $assignment->ip }}:{{ $assignment->port }}" @if($server->allocation_id === $assignment->id)disabled @endif>{{ $assignment->alias }}:{{ $assignment->port }}</option>
@endforeach
</select>
</div>
<p class="text-muted"><small>Simply select which ports you would like to remove from the list above. If you want to assign a port on a different IP that is already in use you can select it above and delete it down here.</small></p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
{!! csrf_field() !!}
<input type="submit" class="btn btn-sm btn-primary" value="Update Build Configuration" />
</div>
</div>
</form>
</div>
</div>
</div>
<div class="tab-pane" id="tab_startup">
<form action="{{ route('admin.servers.post.startup', $server->id) }}" method="POST">
<div class="panel panel-default">
<div class="panel-heading"></div>
<div class="panel-body">
<div class="row">
<div class="col-md-12">
<div class="alert alert-info">Changing any of the values below will require a restart for them to take effect.</div>
<label class="control-label">Server Startup Command</label>
<div class="input-group">
<span class="input-group-addon">{{ $server->option->display_executable }}</span>
<input type="text" class="form-control" name="startup" value="{{ old('startup', $server->startup) }}" />
</div>
<p class="text-muted"><small>The following data replacers are avaliable for the startup command: <code>@{{SERVER_MEMORY}}</code>, <code>@{{SERVER_IP}}</code>, and <code>@{{SERVER_PORT}}</code>. They will be replaced with the allocated memory, server ip, and server port respectively.</small></p>
</div>
</div>
</div>
<div class="panel-heading" style="border-top: 1px solid #ddd;"></div>
<div class="panel-body">
<div class="row">
@foreach($server->option->variables as $variable)
<div class="form-group col-md-6">
<label class="control-label">
@if($variable->required)<span class="label label-primary">Required</span> @endif
@if(! $variable->user_viewable)<span data-toggle="tooltip" data-placement="top" title="Not Visible to Users" class="label label-danger"><i class="fa fa-eye-slash"></i></span> @endif
@if(! $variable->user_editable)<span data-toggle="tooltip" data-placement="top" title="Not Editable by Users" class="label label-danger"><i class="fa fa-edit"></i></span> @endif
{{ $variable->name }}
</label>
<div>
<input type="text" name="{{ $variable->env_variable }}" class="form-control" value="{{ old($variable->env_variable, (! $variable->server_value) ? $variable->default_value : $variable->server_value) }}" data-action="matchRegex" data-regex="{{ $variable->regex }}" />
</div>
<p class="text-muted"><small>{!! $variable->description !!}<br />Regex: <code>{{ $variable->regex }}</code><br />Access as: <code>&#123;&#123;{{ $variable->env_variable }}&#125;&#125;</code></small></p>
</div>
@endforeach
</div>
</div>
<div class="panel-heading" style="border-top: 1px solid #ddd;"></div>
<div class="panel-body">
<div class="row">
<div class="col-md-12">
{!! csrf_field() !!}
<input type="submit" class="btn btn-primary btn-sm" value="Update Startup Arguments" />
</div>
</div>
</div>
</div>
</form>
</div>
<div class="tab-pane" id="tab_database">
<div class="panel panel-default">
<div class="panel-heading"></div>
<div class="panel-body">
<h4 class="nopad">Add New Database</h4>
<form action="{{ route('admin.servers.database', $server->id) }}" method="post">
<div class="row">
<div class="form-group col-md-6">
<label class="control-label">Database Name:</label>
<div class="input-group">
<div class="input-group-addon">s{{ $server->id }}_</div>
<input type="text" name="database" value="{{ old('database') }}" class="form-control">
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">Connections From:</label>
<div>
<input type="text" name="remote" value="{{ old('remote', '%') }}" class="form-control">
</div>
<p class="text-muted"><small>Which IP to allow connections from. Standard MySQL wildcard notation allowed (e.g. <code>192.168.%.%</code>).</small></p>
</div>
</div>
<div class="row">
<div class="col-xs-6">
<label class="control-label">Database Server:</label>
<select name="db_server" class="form-control">
@foreach($db_servers as $dbs)
<option value="{{ $dbs->id }}" @if($dbs->linked_node === $server->node_id)selected="selected"@endif>{{ $dbs->name }} ({{ $dbs->host }}:{{ $dbs->port }})</option>
@endforeach
</select>
</div>
<div class="col-xs-6">
<label class="control-label">&nbsp;</label>
<div>
{!! csrf_field() !!}
<input type="submit" value="Create New Database &rarr;" class="btn btn-sm btn-primary pull-right">
</div>
</div>
</div>
</form>
</div>
@if(count($server->databases) > 0)
<div class="panel-heading" style="border-top: 1px solid #ddd;"></div>
<div class="panel-body">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Database</th>
<th>User (Connections From)</th>
<th>Password</th>
<th>DB Server</th>
<th></th>
</th>
</thead>
<tbody>
@foreach($server->databases as $database)
<tr>
<td>{{ $database->database }}</td>
<td>{{ $database->username }} ({{ $database->remote }})</td>
<td><code>{{ Crypt::decrypt($database->password) }}</code> <a href="#" data-action="reset-database-password" data-id="{{ $database->id }}"><i class="fa fa-refresh pull-right"></i></a></td>
<td><code>{{ $database->host->host }}:{{ $database->host->port }}</code></td>
<td class="text-center"><a href="#delete" data-action="delete_database" data-database="{{ $database->id }}" class="text-danger"><i class="fa fa-trash-o"></i></a></td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
</div>
@endif
@if($server->installed !== 2)
<div class="tab-pane" id="tab_manage">
<div class="panel panel-default">
<div class="panel-heading"></div>
<div class="panel-body">
<div class="row">
<div class="col-md-4 text-center">
<a href="/server/{{ $server->uuidShort }}/" target="_blank">
<button type="submit" class="btn btn-sm btn-primary">Manage Server</button>
</a>
</div>
<div class="col-md-8">
<p>This will take you to the server management page that users normally see and allow you to manage server files as well as check the console and data usage.</p>
</div>
</div>
</div>
<div class="panel-heading" style="border-top: 1px solid #ddd;"></div>
<div class="panel-body">
<div class="row">
<div class="col-md-4 text-center">
<form action="/admin/servers/view/{{ $server->id }}/installed" method="POST">
{!! csrf_field() !!}
<button type="submit" class="btn btn-sm btn-primary">Toggle Install Status</button>
</form>
</div>
<div class="col-md-8">
<p>This will toggle the install status for the server.</p>
<div class="alert alert-warning">If you have just created this server it is ill advised to perform this action as the daemon will contact the panel when finished which could cause the install status to be wrongly set.</div>
</div>
</div>
</div>
@if($server->installed === 1)
<div class="panel-heading" style="border-top: 1px solid #ddd;"></div>
<div class="panel-body">
<div class="row">
<div class="col-md-4 text-center">
<form action="/admin/servers/view/{{ $server->id }}/rebuild" method="POST">
{!! csrf_field() !!}
<button type="submit" class="btn btn-sm btn-primary">Rebuild Server Container</button>
</form>
</div>
<div class="col-md-8">
<p>This will trigger a rebuild of the server container when it next starts up. This is useful if you modified the server configuration file manually, or something just didn't work out correctly.</p>
<div class="alert alert-info">A rebuild will automatically occur whenever you edit build configuration settings for the server.</div>
</div>
</div>
</div>
@endif
<div class="panel-heading" style="border-top: 1px solid #ddd;"></div>
<div class="panel-body">
<div class="row">
@if($server->suspended === 0)
<div class="col-md-4 text-center">
<form action="/admin/servers/view/{{ $server->id }}/suspend" method="POST">
{!! csrf_field() !!}
<button type="submit" class="btn btn-sm btn-warning">Suspend Server</button>
</form>
</div>
<div class="col-md-8">
<p>This will suspend the server, stop any running processes, and immediately block the user from being able to access their files or otherwise manage the server through the panel or API.</p>
</div>
@else
<div class="col-md-4 text-center">
<form action="/admin/servers/view/{{ $server->id }}/unsuspend" method="POST">
{!! csrf_field() !!}
<button type="submit" class="btn btn-sm btn-success">Unsuspend Server</button>
</form>
</div>
<div class="col-md-8">
<p>This will unsuspend the server and restore normal user access.</p>
</div>
@endif
</div>
</div>
</div>
</div>
@endif
<div class="tab-pane" id="tab_delete">
<div class="panel panel-default">
<div class="panel-heading"></div>
@if($server->installed === 1)
<div class="panel-body">
<div class="row">
<div class="col-md-4 text-center">
<form action="/admin/servers/view/{{ $server->id }}" method="POST" data-attr="deleteServer">
{!! csrf_field() !!}
{!! method_field('DELETE') !!}
<button type="submit" class="btn btn-sm btn-danger">Delete Server</button>
</form>
</div>
<div class="col-md-8">
<div class="alert alert-danger">Deleting a server is an irreversible action. <strong>All data will be immediately removed relating to this server.</strong></div>
</div>
</div>
</div>
<div class="panel-heading" style="border-top: 1px solid #ddd;"></div>
@endif
<div class="panel-body">
<div class="row">
<div class="col-md-4 text-center">
<form action="/admin/servers/view/{{ $server->id }}/force" method="POST" data-attr="deleteServer">
{!! csrf_field() !!}
{!! method_field('DELETE') !!}
<button type="submit" class="btn btn-sm btn-danger">Force Delete Server</button>
</form>
</div>
<div class="col-md-8">
<div class="alert alert-danger">This is the same as deleting a server, however, if an error is returned by the daemon it is ignored and the server is still removed from the panel.</strong></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$('[data-toggle="tooltip"]').tooltip();
$('#sidebar_links').find("a[href='/admin/servers']").addClass('active');
(function checkServerInfo() {
$.ajax({
type: 'GET',
headers: {
'X-Access-Token': '{{ $server->daemonSecret }}',
'X-Access-Server': '{{ $server->uuid }}'
},
url: '{{ $server->node->scheme }}://{{ $server->node->fqdn }}:{{ $server->node->daemonListen }}/server',
dataType: 'json',
timeout: 5000,
}).done(function (data) {
$('td[data-attr="container-id"]').html('<code>' + data.container.id + '</code>');
$('td[data-attr="container-user"]').html('<code>' + data.user + '</code>');
}).fail(function (jqXHR) {
$('td[data-attr="container-id"]').html('<code>error</code>');
$('td[data-attr="container-user"]').html('<code>error</code>');
console.error(jqXHR);
}).always(function () {
setTimeout(checkServerInfo, 60000);
})
})();
$('input[name="default"]').on('change', function (event) {
$('select[name="remove_additional[]"]').find('option:disabled').prop('disabled', false);
$('select[name="remove_additional[]"]').find('option[value="' + $(this).val() + '"]').prop('disabled', true).prop('selected', false);
});
$('[data-action="matchRegex"]').keyup(function (event) {
if (!$(this).data('regex')) return;
var input = $(this).val();
var regex = new RegExp(escapeRegExp($(this).data('regex')));
console.log(regex);
if (!regex.test(input)) {
$(this).parent().parent().removeClass('has-success').addClass('has-error');
} else {
$(this).parent().parent().removeClass('has-error').addClass('has-success');
}
});
$('form[data-attr="deleteServer"]').submit(function (event) {
event.preventDefault();
swal({
title: '',
type: 'warning',
text: 'Are you sure that you want to delete this server? There is no going back, all data will immediately be removed.',
showCancelButton: true,
confirmButtonText: 'Delete',
confirmButtonColor: '#d9534f',
closeOnConfirm: false
}, function () {
event.target.submit();
});
});
$('[data-action="delete_database"]').click(function (event) {
event.preventDefault();
var self = $(this);
swal({
title: '',
type: 'warning',
text: 'Are you sure that you want to delete this database? There is no going back, all data will immediately be removed.',
showCancelButton: true,
confirmButtonText: 'Delete',
confirmButtonColor: '#d9534f',
closeOnConfirm: false
}, function () {
$.ajax({
method: 'DELETE',
url: '{{ route('admin.databases') }}/delete/' + self.data('database'),
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
}).done(function () {
self.parent().parent().slideUp();
swal({
title: '',
type: 'success',
text: 'Successfully deleted this database.'
});
}).fail(function (jqXHR) {
console.error(jqXHR);
swal({
type: 'error',
title: 'Whoops!',
text: (typeof jqXHR.responseJSON.error !== 'undefined') ? jqXHR.responseJSON.error : 'An error occured while processing this request.'
});
});
});
});
$('[data-action="reset-database-password"]').click(function (e) {
e.preventDefault();
var block = $(this);
$(this).find('i').addClass('fa-spin');
$.ajax({
type: 'POST',
url: '{{ route('server.ajax.reset-database-password', $server->uuidShort) }}',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
data: {
'database': $(this).data('id')
}
}).done(function (data) {
block.parent().find('code').html(data);
}).fail(function(jqXHR, textStatus, errorThrown) {
console.error(jqXHR);
var error = 'An error occured while trying to process this request.';
if (typeof jqXHR.responseJSON !== 'undefined' && typeof jqXHR.responseJSON.error !== 'undefined') {
error = jqXHR.responseJSON.error;
}
swal({
type: 'error',
title: 'Whoops!',
text: error
});
}).always(function () {
block.find('i').removeClass('fa-spin');
});
});
});
</script>
@endsection

View File

@ -1,122 +0,0 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}}
{{-- of this software and associated documentation files (the "Software"), to deal --}}
{{-- in the Software without restriction, including without limitation the rights --}}
{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}}
{{-- copies of the Software, and to permit persons to whom the Software is --}}
{{-- furnished to do so, subject to the following conditions: --}}
{{-- The above copyright notice and this permission notice shall be included in all --}}
{{-- copies or substantial portions of the Software. --}}
{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}}
{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}}
{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}}
{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}}
{{-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --}}
{{-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --}}
{{-- SOFTWARE. --}}
@extends('layouts.admin')
@section('title')
New Variable for {{ $option->name }}
@endsection
@section('content')
<div class="col-md-12">
<ul class="breadcrumb">
<li><a href="/admin">Admin Control</a></li>
<li><a href="/admin/services">Services</a></li>
<li><a href="{{ route('admin.services.service', $option->service->id) }}">{{ $option->service->name }}</a></li>
<li><a href="{{ route('admin.services.option', [$option->service->id, $option->id]) }}">{{ $option->name }}</a></li>
<li class="active">New Variable</li>
</ul>
<h3>New Option Variable</h3><hr />
<form action="{{ route('admin.services.option.variable.new', [$option->service->id, $option->id]) }}" method="POST">
<div class="well">
<div class="row">
<div class="col-md-12 form-group">
<label class="control-label">Variable Name:</label>
<div>
<input type="text" name="name" class="form-control" value="{{ old('name') }}" />
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 form-group">
<label class="control-label">Variable Description:</label>
<div>
<textarea name="description" class="form-control" rows="4">{{ old('description') }}</textarea>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 form-group">
<label class="control-label">Regex:</label>
<div>
<input type="text" name="regex" class="form-control" value="{{ old('regex') }}" />
<p class="text-muted"><small>Regex code to use when verifying the contents of the field.</small></p>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 form-group">
<label class="control-label">Environment Variable:</label>
<div>
<input type="text" name="env_variable" id="env_var" class="form-control" value="{{ old('env_variable') }}" />
<p class="text-muted"><small>Accessed in startup by using <code>&#123;&#123;&#125;&#125;</code> parameter.</small></p>
</div>
</div>
<div class="col-md-6 form-group">
<label class="control-label">Default Value:</label>
<div>
<input type="text" name="default_value" class="form-control" value="{{ old('default_value') }}" />
<p class="text-muted"><small>The default value to use for this field.</small></p>
</div>
</div>
</div>
<div class="row fuelux">
<div class="col-md-4">
<div class="checkbox highlight">
<label class="checkbox-custom highlight" data-initialize="checkbox">
<input class="sr-only" name="user_viewable" type="checkbox" value="1" @if((int) old('user_viewable') === 1)checked="checked"@endif> <strong>User Viewable</strong>
<p class="text-muted"><small>Can users view this variable?</small><p>
</label>
</div>
</div>
<div class="col-md-4">
<div class="checkbox highlight">
<label class="checkbox-custom highlight" data-initialize="checkbox">
<input class="sr-only" name="user_editable" type="checkbox" value="1" @if((int) old('user_editable') === 1)checked="checked"@endif> <strong>User Editable</strong>
<p class="text-muted"><small>Can users edit this variable?</small><p>
</label>
</div>
</div>
<div class="col-md-4">
<div class="checkbox highlight">
<label class="checkbox-custom highlight" data-initialize="checkbox">
<input class="sr-only" name="required" type="checkbox" value="1" @if((int) old('required') === 1)checked="checked"@endif> <strong>Required</strong>
<p class="text-muted"><small>This this variable required?</small><p>
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
{!! csrf_field() !!}
<input type="submit" class="btn btn-sm btn-primary" value="Add Variable" />
</div>
</div>
</div>
</form>
</div>
<script>
$(document).ready(function () {
$('#sidebar_links').find("a[href='/admin/services']").addClass('active');
$('#env_var').on('keyup', function () {
$(this).parent().find('code').html('&#123;&#123;' + escape($(this).val()) + '&#125;&#125;');
});
});
</script>
@endsection

View File

@ -0,0 +1,31 @@
'use strict';
/**
* Pterodactyl - Daemon
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
const rfr = require('rfr');
const Core = rfr('src/services/index.js');
class Service extends Core {}
module.exports = Service;