diff --git a/.editorconfig b/.editorconfig
index c3f635323..bc49d523e 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,11 +1,12 @@
root = true
[*]
+end_of_line = lf
+insert_final_newline = true
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
-insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33682bd80..dd10b6458 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,21 @@ This file is a running track of new features and fixes to each version of the pa
This project follows [Semantic Versioning](http://semver.org) guidelines.
+## v0.6.0-pre.1
+### Added
+* Remote routes for daemon to contact in order to allow Daemon to retrieve updated service configuration files on boot. Centralizes services to the panel rather than to each daemon.
+* Basic service pack implementation to allow assignment of modpacks or software to a server to pre-install applications and allow users to update.
+* Users can now have a username as well as client name assigned to thier account.
+
+### Fixed
+* Bug causing error logs to be spammed if someone timed out on an ajax based page.
+
+### Changed
+* Admin API and base routes for user management now define the fields that should be passed to repositories rather than passing all fields.
+* User model now defines mass assignment fields using `$fillable` rather than `$guarded`.
+
+### Deprecated
+
## v0.5.6 (Bodacious Boreopterus)
### Added
* Added the following languages: Estonian `et`, Dutch `nl`, Norwegian `nb` (partial), Romanian `ro`, and Russian `ru`. Interested in helping us translate the panel into more languages, or improving existing translations? Contact us on Discord and let us know.
diff --git a/app/Console/Commands/CleanServiceBackup.php b/app/Console/Commands/CleanServiceBackup.php
new file mode 100644
index 000000000..93af785f4
--- /dev/null
+++ b/app/Console/Commands/CleanServiceBackup.php
@@ -0,0 +1,74 @@
+.
+ *
+ * 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.
+ */
+
+namespace Pterodactyl\Console\Commands;
+
+use Carbon;
+use Storage;
+use Illuminate\Console\Command;
+
+class CleanServiceBackup extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'pterodactyl:cleanservices';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Cleans .bak files assocaited with service backups whene editing files through the panel.';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $files = Storage::files('services/.bak');
+
+ foreach ($files as $file) {
+ $lastModified = Carbon::createFromTimestamp(Storage::lastModified($file));
+ if ($lastModified->diffInMinutes(Carbon::now()) > 5) {
+ $this->info('Deleting ' . $file);
+ Storage::delete($file);
+ }
+ }
+ }
+}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index c8276b447..53f80281d 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -21,6 +21,7 @@ class Kernel extends ConsoleKernel
\Pterodactyl\Console\Commands\ClearTasks::class,
\Pterodactyl\Console\Commands\ClearServices::class,
\Pterodactyl\Console\Commands\UpdateEmailSettings::class,
+ \Pterodactyl\Console\Commands\CleanServiceBackup::class,
];
/**
@@ -33,5 +34,6 @@ class Kernel extends ConsoleKernel
{
$schedule->command('pterodactyl:tasks')->everyMinute()->withoutOverlapping();
$schedule->command('pterodactyl:tasks:clearlog')->twiceDaily(3, 15);
+ $schedule->command('pterodactyl:cleanservices')->twiceDaily(1, 13);
}
}
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index 2b89cacbf..fedfa1c4b 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -46,13 +46,12 @@ class Handler extends ExceptionHandler
*/
public function render($request, Exception $exception)
{
- if ($request->isXmlHttpRequest() || $request->ajax() || $request->is('remote/*')) {
+ if ($request->expectsJson()) {
$response = response()->json([
'error' => ($exception instanceof DisplayException) ? $exception->getMessage() : 'An unhandled error occured while attempting to process this request.',
- ], 500);
+ ], ($this->isHttpException($exception)) ? $e->getStatusCode() : 500);
- // parent::render() will log it, we are bypassing it in this case.
- Log::error($exception);
+ parent::report($exception);
}
return (isset($response)) ? $response : parent::render($request, $exception);
diff --git a/app/Http/Controllers/API/UserController.php b/app/Http/Controllers/API/UserController.php
index 59e9af975..c3a658a0e 100755
--- a/app/Http/Controllers/API/UserController.php
+++ b/app/Http/Controllers/API/UserController.php
@@ -122,6 +122,9 @@ class UserController extends BaseController
{
try {
$user = new UserRepository;
+ $create = $user->create($request->only([
+ 'email', 'username', 'name_first', 'name_last', 'password', 'root_admin', 'custom_id',
+ ]));
$create = $user->create($request->input('email'), $request->input('password'), $request->input('admin'), $request->input('custom_id'));
return ['id' => $create];
@@ -156,7 +159,9 @@ class UserController extends BaseController
{
try {
$user = new UserRepository;
- $user->update($id, $request->all());
+ $user->update($id, $request->only([
+ 'username', 'email', 'name_first', 'name_last', 'password', 'root_admin', 'language',
+ ]));
return Models\User::findOrFail($id);
} catch (DisplayValidationException $ex) {
diff --git a/app/Http/Controllers/Admin/PackController.php b/app/Http/Controllers/Admin/PackController.php
new file mode 100644
index 000000000..fdf1b5a1a
--- /dev/null
+++ b/app/Http/Controllers/Admin/PackController.php
@@ -0,0 +1,252 @@
+.
+ *
+ * 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.
+ */
+
+namespace Pterodactyl\Http\Controllers\Admin;
+
+use DB;
+use Log;
+use Alert;
+use Storage;
+use Pterodactyl\Models;
+use Illuminate\Http\Request;
+use Pterodactyl\Exceptions\DisplayException;
+use Pterodactyl\Http\Controllers\Controller;
+use Pterodactyl\Repositories\ServiceRepository\Pack;
+use Pterodactyl\Exceptions\DisplayValidationException;
+
+class PackController extends Controller
+{
+ public function __construct()
+ {
+ //
+ }
+
+ protected function formatServices()
+ {
+ $options = Models\ServiceOptions::select(
+ 'services.name AS p_service',
+ 'service_options.id',
+ 'service_options.name'
+ )->join('services', 'services.id', '=', 'service_options.parent_service')->get();
+
+ $array = [];
+ foreach ($options as &$option) {
+ if (! array_key_exists($option->p_service, $array)) {
+ $array[$option->p_service] = [];
+ }
+
+ $array[$option->p_service] = array_merge($array[$option->p_service], [[
+ 'id' => $option->id,
+ 'name' => $option->name,
+ ]]);
+ }
+
+ return $array;
+ }
+
+ public function listAll(Request $request)
+ {
+ return view('admin.services.packs.index', [
+ 'services' => Models\Service::all(),
+ ]);
+ }
+
+ public function listByOption(Request $request, $id)
+ {
+ $option = Models\ServiceOptions::findOrFail($id);
+
+ return view('admin.services.packs.byoption', [
+ 'packs' => Models\ServicePack::where('option', $option->id)->get(),
+ 'service' => Models\Service::findOrFail($option->parent_service),
+ 'option' => $option,
+ ]);
+ }
+
+ public function listByService(Request $request, $id)
+ {
+ return view('admin.services.packs.byservice', [
+ 'service' => Models\Service::findOrFail($id),
+ 'options' => Models\ServiceOptions::select(
+ 'service_options.id',
+ 'service_options.name',
+ DB::raw('(SELECT COUNT(id) FROM service_packs WHERE service_packs.option = service_options.id) AS p_count')
+ )->where('parent_service', $id)->get(),
+ ]);
+ }
+
+ public function new(Request $request, $opt = null)
+ {
+ return view('admin.services.packs.new', [
+ 'services' => $this->formatServices(),
+ 'packFor' => $opt,
+ ]);
+ }
+
+ public function create(Request $request)
+ {
+ try {
+ $repo = new Pack;
+ $id = $repo->create($request->except([
+ '_token',
+ ]));
+ Alert::success('Successfully created new service!')->flash();
+
+ return redirect()->route('admin.services.packs.edit', $id)->withInput();
+ } catch (DisplayValidationException $ex) {
+ return redirect()->route('admin.services.packs.new', $request->input('option'))->withErrors(json_decode($ex->getMessage()))->withInput();
+ } catch (DisplayException $ex) {
+ Alert::danger($ex->getMessage())->flash();
+ } catch (\Exception $ex) {
+ Log::error($ex);
+ Alert::danger('An error occured while attempting to add a new service pack.')->flash();
+ }
+
+ return redirect()->route('admin.services.packs.new', $request->input('option'))->withInput();
+ }
+
+ public function edit(Request $request, $id)
+ {
+ $pack = Models\ServicePack::findOrFail($id);
+ $option = Models\ServiceOptions::select('id', 'parent_service', 'name')->where('id', $pack->option)->first();
+
+ return view('admin.services.packs.edit', [
+ 'pack' => $pack,
+ 'services' => $this->formatServices(),
+ 'files' => Storage::files('packs/' . $pack->uuid),
+ 'service' => Models\Service::findOrFail($option->parent_service),
+ 'option' => $option,
+ ]);
+ }
+
+ public function update(Request $request, $id)
+ {
+ if (! is_null($request->input('action_delete'))) {
+ try {
+ $repo = new Pack;
+ $repo->delete($id);
+ Alert::success('The requested service pack has been deleted from the system.')->flash();
+
+ return redirect()->route('admin.services.packs');
+ } catch (DisplayException $ex) {
+ Alert::danger($ex->getMessage())->flash();
+ } catch (\Exception $ex) {
+ Log::error($ex);
+ Alert::danger('An error occured while attempting to delete this pack.')->flash();
+ }
+
+ return redirect()->route('admin.services.packs.edit', $id);
+ } else {
+ try {
+ $repo = new Pack;
+ $repo->update($id, $request->except([
+ '_token',
+ ]));
+ Alert::success('Service pack has been successfully updated.')->flash();
+ } catch (DisplayValidationException $ex) {
+ return redirect()->route('admin.services.packs.edit', $id)->withErrors(json_decode($ex->getMessage()))->withInput();
+ } catch (\Exception $ex) {
+ Log::error($ex);
+ Alert::danger('An error occured while attempting to add edit this pack.')->flash();
+ }
+
+ return redirect()->route('admin.services.packs.edit', $id);
+ }
+ }
+
+ public function export(Request $request, $id, $files = false)
+ {
+ $pack = Models\ServicePack::findOrFail($id);
+ $json = [
+ 'name' => $pack->name,
+ 'version' => $pack->version,
+ 'description' => $pack->dscription,
+ 'selectable' => (bool) $pack->selectable,
+ 'visible' => (bool) $pack->visible,
+ 'build' => [
+ 'memory' => $pack->build_memory,
+ 'swap' => $pack->build_swap,
+ 'cpu' => $pack->build_cpu,
+ 'io' => $pack->build_io,
+ 'container' => $pack->build_container,
+ 'script' => $pack->build_script,
+ ],
+ ];
+
+ $filename = tempnam(sys_get_temp_dir(), 'pterodactyl_');
+ if ((bool) $files) {
+ $zip = new \ZipArchive;
+ if (! $zip->open($filename, \ZipArchive::CREATE)) {
+ abort(503, 'Unable to open file for writing.');
+ }
+
+ $files = Storage::files('packs/' . $pack->uuid);
+ foreach ($files as $file) {
+ $zip->addFile(storage_path('app/' . $file), basename(storage_path('app/' . $file)));
+ }
+
+ $zip->addFromString('import.json', json_encode($json, JSON_PRETTY_PRINT));
+ $zip->close();
+
+ return response()->download($filename, 'pack-' . $pack->name . '.zip')->deleteFileAfterSend(true);
+ } else {
+ $fp = fopen($filename, 'a+');
+ fwrite($fp, json_encode($json, JSON_PRETTY_PRINT));
+ fclose($fp);
+
+ return response()->download($filename, 'pack-' . $pack->name . '.json', [
+ 'Content-Type' => 'application/json',
+ ])->deleteFileAfterSend(true);
+ }
+ }
+
+ public function uploadForm(Request $request, $for = null)
+ {
+ return view('admin.services.packs.upload', [
+ 'services' => $this->formatServices(),
+ 'for' => $for,
+ ]);
+ }
+
+ public function postUpload(Request $request)
+ {
+ try {
+ $repo = new Pack;
+ $id = $repo->createWithTemplate($request->except([
+ '_token',
+ ]));
+ Alert::success('Successfully created new service!')->flash();
+
+ return redirect()->route('admin.services.packs.edit', $id)->withInput();
+ } catch (DisplayValidationException $ex) {
+ return redirect()->back()->withErrors(json_decode($ex->getMessage()))->withInput();
+ } catch (DisplayException $ex) {
+ Alert::danger($ex->getMessage())->flash();
+ } catch (\Exception $ex) {
+ Log::error($ex);
+ Alert::danger('An error occured while attempting to add a new service pack.')->flash();
+ }
+
+ return redirect()->back();
+ }
+}
diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php
index dd007881c..2dce69589 100644
--- a/app/Http/Controllers/Admin/ServersController.php
+++ b/app/Http/Controllers/Admin/ServersController.php
@@ -253,7 +253,7 @@ class ServersController extends Controller
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
- public function postNewServerServiceVariables(Request $request)
+ public function postNewServerOptionDetails(Request $request)
{
if (! $request->input('option')) {
return response()->json([
@@ -269,6 +269,7 @@ class ServersController extends Controller
->first();
return response()->json([
+ 'packs' => Models\ServicePack::select('id', 'name', 'version')->where('option', $request->input('option'))->where('selectable', true)->get(),
'variables' => Models\ServiceVariables::where('option_id', $request->input('option'))->get(),
'exec' => $option->executable,
'startup' => $option->startup,
diff --git a/app/Http/Controllers/Admin/ServiceController.php b/app/Http/Controllers/Admin/ServiceController.php
index e548ae07a..4e1b3b1b3 100644
--- a/app/Http/Controllers/Admin/ServiceController.php
+++ b/app/Http/Controllers/Admin/ServiceController.php
@@ -27,6 +27,7 @@ namespace Pterodactyl\Http\Controllers\Admin;
use DB;
use Log;
use Alert;
+use Storage;
use Pterodactyl\Models;
use Illuminate\Http\Request;
use Pterodactyl\Exceptions\DisplayException;
@@ -286,4 +287,39 @@ class ServiceController extends Controller
return redirect()->route('admin.services.option', [$service, $option]);
}
+
+ public function getConfiguration(Request $request, $serviceId)
+ {
+ $service = Models\Service::findOrFail($serviceId);
+
+ return view('admin.services.config', [
+ 'service' => $service,
+ 'contents' => [
+ 'json' => Storage::get('services/' . $service->file . '/main.json'),
+ 'index' => Storage::get('services/' . $service->file . '/index.js'),
+ ],
+ ]);
+ }
+
+ public function postConfiguration(Request $request, $serviceId)
+ {
+ try {
+ $repo = new ServiceRepository\Service;
+ $repo->updateFile($serviceId, $request->except([
+ '_token',
+ ]));
+
+ return response('', 204);
+ } catch (DisplayException $ex) {
+ return response()->json([
+ 'error' => $ex->getMessage(),
+ ], 503);
+ } catch (\Exception $ex) {
+ Log::error($ex);
+
+ return response()->json([
+ 'error' => 'An error occured while attempting to save the file.',
+ ], 503);
+ }
+ }
}
diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php
index 36e2590ba..8e4425a80 100644
--- a/app/Http/Controllers/Admin/UserController.php
+++ b/app/Http/Controllers/Admin/UserController.php
@@ -116,7 +116,13 @@ class UserController extends Controller
{
try {
$user = new UserRepository;
- $userid = $user->create($request->input('email'), $request->input('password'));
+ $userid = $user->create($request->only([
+ 'email',
+ 'password',
+ 'name_first',
+ 'name_last',
+ 'username',
+ ]));
Alert::success('Account has been successfully created.')->flash();
return redirect()->route('admin.users.view', $userid);
@@ -132,19 +138,16 @@ class UserController extends Controller
public function updateUser(Request $request, $user)
{
- $data = [
- 'email' => $request->input('email'),
- 'root_admin' => $request->input('root_admin'),
- 'password_confirmation' => $request->input('password_confirmation'),
- ];
-
- if ($request->input('password')) {
- $data['password'] = $request->input('password');
- }
-
try {
$repo = new UserRepository;
- $repo->update($user, $data);
+ $repo->update($user, $request->only([
+ 'email',
+ 'password',
+ 'name_first',
+ 'name_last',
+ 'username',
+ 'root_admin',
+ ]));
Alert::success('User account was successfully updated.')->flash();
} catch (DisplayValidationException $ex) {
return redirect()->route('admin.users.view', $user)->withErrors(json_decode($ex->getMessage()));
diff --git a/app/Http/Controllers/Daemon/ServiceController.php b/app/Http/Controllers/Daemon/ServiceController.php
new file mode 100644
index 000000000..63c449f2d
--- /dev/null
+++ b/app/Http/Controllers/Daemon/ServiceController.php
@@ -0,0 +1,79 @@
+.
+ *
+ * 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.
+ */
+
+namespace Pterodactyl\Http\Controllers\Daemon;
+
+use Storage;
+use Pterodactyl\Models;
+use Illuminate\Http\Request;
+use Pterodactyl\Http\Controllers\Controller;
+
+class ServiceController extends Controller
+{
+ /**
+ * Controller Constructor.
+ */
+ public function __construct()
+ {
+ //
+ }
+
+ /**
+ * Returns a listing of all services currently on the system,
+ * as well as the associated files and the file hashes for
+ * caching purposes.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @return \Illuminate\Http\Response
+ */
+ public function list(Request $request)
+ {
+ $response = [];
+ foreach (Models\Service::all() as &$service) {
+ $response[$service->file] = [
+ 'main.json' => sha1_file(storage_path('app/services/' . $service->file . '/main.json')),
+ 'index.js' => sha1_file(storage_path('app/services/' . $service->file . '/index.js')),
+ ];
+ }
+
+ return response()->json($response);
+ }
+
+ /**
+ * Returns the contents of the requested file for the given service.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param string $service
+ * @param string $file
+ * @return \Illuminate\Http\Response
+ */
+ public function pull(Request $request, $service, $file)
+ {
+ if (! Storage::exists('services/' . $service . '/' . $file)) {
+ return response()->json(['error' => 'No such file.'], 404);
+ }
+
+ return response()->file(storage_path('app/services/' . $service . '/' . $file));
+ }
+}
diff --git a/app/Http/Routes/AdminRoutes.php b/app/Http/Routes/AdminRoutes.php
index 3b94b2aeb..731052238 100644
--- a/app/Http/Routes/AdminRoutes.php
+++ b/app/Http/Routes/AdminRoutes.php
@@ -147,8 +147,8 @@ class AdminRoutes
'uses' => 'Admin\ServersController@postNewServerServiceOptions',
]);
- $router->post('/new/service-variables', [
- 'uses' => 'Admin\ServersController@postNewServerServiceVariables',
+ $router->post('/new/option-details', [
+ 'uses' => 'Admin\ServersController@postNewServerOptionDetails',
]);
// End Assorted Page Helpers
@@ -383,6 +383,15 @@ class AdminRoutes
'uses' => 'Admin\ServiceController@deleteService',
]);
+ $router->get('/service/{id}/configuration', [
+ 'as' => 'admin.services.service.config',
+ 'uses' => 'Admin\ServiceController@getConfiguration',
+ ]);
+
+ $router->post('/service/{id}/configuration', [
+ 'uses' => 'Admin\ServiceController@postConfiguration',
+ ]);
+
$router->get('/service/{service}/option/new', [
'as' => 'admin.services.option.new',
'uses' => 'Admin\ServiceController@newOption',
@@ -424,5 +433,53 @@ class AdminRoutes
'uses' => 'Admin\ServiceController@deleteVariable',
]);
});
+
+ // Service Packs
+ $router->group([
+ 'prefix' => 'admin/services/packs',
+ 'middleware' => [
+ 'auth',
+ 'admin',
+ 'csrf',
+ ],
+ ], function () use ($router) {
+ $router->get('/new/{option?}', [
+ 'as' => 'admin.services.packs.new',
+ 'uses' => 'Admin\PackController@new',
+ ]);
+ $router->post('/new', [
+ 'uses' => 'Admin\PackController@create',
+ ]);
+ $router->get('/upload/{option?}', [
+ 'as' => 'admin.services.packs.uploadForm',
+ 'uses' => 'Admin\PackController@uploadForm',
+ ]);
+ $router->post('/upload', [
+ 'uses' => 'Admin\PackController@postUpload',
+ ]);
+ $router->get('/', [
+ 'as' => 'admin.services.packs',
+ 'uses' => 'Admin\PackController@listAll',
+ ]);
+ $router->get('/for/option/{option}', [
+ 'as' => 'admin.services.packs.option',
+ 'uses' => 'Admin\PackController@listByOption',
+ ]);
+ $router->get('/for/service/{service}', [
+ 'as' => 'admin.services.packs.service',
+ 'uses' => 'Admin\PackController@listByService',
+ ]);
+ $router->get('/edit/{pack}', [
+ 'as' => 'admin.services.packs.edit',
+ 'uses' => 'Admin\PackController@edit',
+ ]);
+ $router->post('/edit/{pack}', [
+ 'uses' => 'Admin\PackController@update',
+ ]);
+ $router->get('/edit/{pack}/export/{archive?}', [
+ 'as' => 'admin.services.packs.export',
+ 'uses' => 'Admin\PackController@export',
+ ]);
+ });
}
}
diff --git a/app/Http/Routes/DaemonRoutes.php b/app/Http/Routes/DaemonRoutes.php
new file mode 100644
index 000000000..ab8b733ab
--- /dev/null
+++ b/app/Http/Routes/DaemonRoutes.php
@@ -0,0 +1,45 @@
+.
+ *
+ * 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.
+ */
+
+namespace Pterodactyl\Http\Routes;
+
+use Illuminate\Routing\Router;
+
+class DaemonRoutes
+{
+ public function map(Router $router)
+ {
+ $router->group(['prefix' => 'daemon'], function () use ($router) {
+ $router->get('services', [
+ 'as' => 'daemon.services',
+ 'uses' => 'Daemon\ServiceController@list',
+ ]);
+
+ $router->get('services/pull/{service}/{file}', [
+ 'as' => 'remote.install',
+ 'uses' => 'Daemon\ServiceController@pull',
+ ]);
+ });
+ }
+}
diff --git a/app/Models/Checksum.php b/app/Models/Checksum.php
new file mode 100644
index 000000000..5e775a59f
--- /dev/null
+++ b/app/Models/Checksum.php
@@ -0,0 +1,53 @@
+.
+ *
+ * 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.
+ */
+
+namespace Pterodactyl\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Checksum extends Model
+{
+ /**
+ * The table associated with the model.
+ *
+ * @var string
+ */
+ protected $table = 'checksums';
+
+ /**
+ * Fields that are not mass assignable.
+ *
+ * @var array
+ */
+ protected $guarded = ['id', 'created_at', 'updated_at'];
+
+ /**
+ * Cast values to correct type.
+ *
+ * @var array
+ */
+ protected $casts = [
+ 'service' => 'integer',
+ ];
+}
diff --git a/app/Models/ServicePack.php b/app/Models/ServicePack.php
new file mode 100644
index 000000000..9b5256a3b
--- /dev/null
+++ b/app/Models/ServicePack.php
@@ -0,0 +1,59 @@
+.
+ *
+ * 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.
+ */
+
+namespace Pterodactyl\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class ServicePack extends Model
+{
+ /**
+ * The table associated with the model.
+ *
+ * @var string
+ */
+ protected $table = 'service_packs';
+
+ /**
+ * Fields that are not mass assignable.
+ *
+ * @var array
+ */
+ protected $guarded = ['id', 'created_at', 'updated_at'];
+
+ /**
+ * Cast values to correct type.
+ *
+ * @var array
+ */
+ protected $casts = [
+ 'option' => 'integer',
+ 'build_memory' => 'integer',
+ 'build_swap' => 'integer',
+ 'build_cpu' => 'integer',
+ 'build_io' => 'integer',
+ 'selectable' => 'boolean',
+ 'visible' => 'boolean',
+ ];
+}
diff --git a/app/Models/User.php b/app/Models/User.php
index ef7bda0bd..c13a9d133 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -37,13 +37,24 @@ use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification;
-class User extends Model implements
- AuthenticatableContract,
- AuthorizableContract,
- CanResetPasswordContract
+class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword, Notifiable;
+ /**
+ * The rules for user passwords.
+ *
+ * @var string
+ */
+ const PASSWORD_RULES = 'regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})';
+
+ /**
+ * The regex rules for usernames.
+ *
+ * @var string
+ */
+ const USERNAME_RULES = 'regex:/^([\w\d\.\-]{1,255})$/';
+
/**
* The table associated with the model.
*
@@ -52,11 +63,11 @@ class User extends Model implements
protected $table = 'users';
/**
- * The attributes that are not mass assignable.
+ * A list of mass-assignable variables.
*
- * @var array
+ * @var [type]
*/
- protected $guarded = ['id', 'remeber_token', 'created_at', 'updated_at'];
+ protected $fillable = ['username', 'email', 'name_first', 'name_last', 'password', 'language', 'use_totp', 'totp_secret', 'gravatar'];
/**
* Cast values to correct type.
@@ -66,6 +77,7 @@ class User extends Model implements
protected $casts = [
'root_admin' => 'integer',
'use_totp' => 'integer',
+ 'gravatar' => 'integer',
];
/**
@@ -76,12 +88,10 @@ class User extends Model implements
protected $hidden = ['password', 'remember_token', 'totp_secret'];
/**
- * The rules for user passwords.
+ * Determines if a user has permissions.
*
- * @var string
+ * @return bool
*/
- const PASSWORD_RULES = 'min:8|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})';
-
public function permissions()
{
return $this->hasMany(Permission::class);
diff --git a/app/Repositories/ServerRepository.php b/app/Repositories/ServerRepository.php
index 0310cee93..deb6a1b75 100644
--- a/app/Repositories/ServerRepository.php
+++ b/app/Repositories/ServerRepository.php
@@ -79,8 +79,9 @@ class ServerRepository
'io' => 'required|numeric|min:10|max:1000',
'cpu' => 'required|numeric|min:0',
'disk' => 'required|numeric|min:0',
- 'service' => 'bail|required|numeric|min:1|exists:services,id',
- 'option' => 'bail|required|numeric|min:1|exists:service_options,id',
+ 'service' => 'required|numeric|min:1|exists:services,id',
+ 'option' => 'required|numeric|min:1|exists:service_options,id',
+ 'pack' => 'required|numeric|min:0',
'startup' => 'string',
'custom_image_name' => 'required_if:use_custom_image,on',
'auto_deploy' => 'sometimes|boolean',
@@ -156,6 +157,18 @@ class ServerRepository
throw new DisplayException('The requested service option does not exist for the specified service.');
}
+ // Validate the Pack
+ if ($data['pack'] == 0) {
+ $data['pack'] = null;
+ }
+
+ if (! is_null($data['pack'])) {
+ $pack = Models\ServicePack::where('id', $data['pack'])->where('option', $data['option'])->first();
+ if (! $pack) {
+ throw new DisplayException('The requested service pack does not seem to exist for this combination.');
+ }
+ }
+
// Load up the Service Information
$service = Models\Service::find($option->parent_service);
@@ -247,6 +260,7 @@ class ServerRepository
'allocation' => $allocation->id,
'service' => $data['service'],
'option' => $data['option'],
+ 'pack' => $data['pack'],
'startup' => $data['startup'],
'daemonSecret' => $uuid->generate('servers', 'daemonSecret'),
'image' => (isset($data['custom_image_name'])) ? $data['custom_image_name'] : $option->docker_image,
@@ -311,6 +325,7 @@ class ServerRepository
'service' => [
'type' => $service->file,
'option' => $option->tag,
+ 'pack' => (isset($pack)) ? $pack->uuid : null,
],
'keys' => [
(string) $server->daemonSecret => $this->daemonPermissions,
diff --git a/app/Repositories/ServiceRepository/Pack.php b/app/Repositories/ServiceRepository/Pack.php
new file mode 100644
index 000000000..61327261f
--- /dev/null
+++ b/app/Repositories/ServiceRepository/Pack.php
@@ -0,0 +1,237 @@
+.
+ *
+ * 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.
+ */
+
+namespace Pterodactyl\Repositories\ServiceRepository;
+
+use DB;
+use Uuid;
+use Storage;
+use Validator;
+use Pterodactyl\Models;
+use Pterodactyl\Services\UuidService;
+use Pterodactyl\Exceptions\DisplayException;
+use Pterodactyl\Exceptions\DisplayValidationException;
+
+class Pack
+{
+ public function __construct()
+ {
+ //
+ }
+
+ public function create(array $data)
+ {
+ $validator = Validator::make($data, [
+ 'name' => 'required|string',
+ 'version' => 'required|string',
+ 'description' => 'sometimes|nullable|string',
+ 'option' => 'required|exists:service_options,id',
+ 'selectable' => 'sometimes|boolean',
+ 'visible' => 'sometimes|boolean',
+ 'build_memory' => 'required|integer|min:0',
+ 'build_swap' => 'required|integer|min:0',
+ 'build_cpu' => 'required|integer|min:0',
+ 'build_io' => 'required|integer|min:10|max:1000',
+ 'build_container' => 'required|string',
+ 'build_script' => 'sometimes|nullable|string',
+ ]);
+
+ if ($validator->fails()) {
+ throw new DisplayValidationException($validator->errors());
+ }
+
+ if (isset($data['file_upload'])) {
+ if (! $data['file_upload']->isValid()) {
+ throw new DisplayException('The file provided does not appear to be valid.');
+ }
+
+ if (! in_array($data['file_upload']->getMimeType(), [
+ 'application/zip',
+ 'application/gzip',
+ ])) {
+ throw new DisplayException('The file provided does not meet the required filetypes of application/zip or application/gzip.');
+ }
+ }
+
+ DB::beginTransaction();
+ try {
+ $uuid = new UuidService;
+ $pack = Models\ServicePack::create([
+ 'option' => $data['option'],
+ 'uuid' => $uuid->generate('servers', 'uuid'),
+ 'build_memory' => $data['build_memory'],
+ 'build_swap' => $data['build_swap'],
+ 'build_cpu' => $data['build_swap'],
+ 'build_io' => $data['build_io'],
+ 'build_script' => (empty($data['build_script'])) ? null : $data['build_script'],
+ 'build_container' => $data['build_container'],
+ 'name' => $data['name'],
+ 'version' => $data['version'],
+ 'description' => (empty($data['description'])) ? null : $data['description'],
+ 'selectable' => isset($data['selectable']),
+ 'visible' => isset($data['visible']),
+ ]);
+
+ Storage::makeDirectory('packs/' . $pack->uuid);
+ if (isset($data['file_upload'])) {
+ $filename = ($data['file_upload']->getMimeType() === 'application/zip') ? 'archive.zip' : 'archive.tar.gz';
+ $data['file_upload']->storeAs('packs/' . $pack->uuid, $filename);
+ }
+
+ DB::commit();
+ } catch (\Exception $ex) {
+ DB::rollBack();
+ throw $ex;
+ }
+
+ return $pack->id;
+ }
+
+ public function createWithTemplate(array $data)
+ {
+ if (! isset($data['file_upload'])) {
+ throw new DisplayException('No template file was found submitted with this request.');
+ }
+
+ if (! $data['file_upload']->isValid()) {
+ throw new DisplayException('The file provided does not appear to be valid.');
+ }
+
+ if (! in_array($data['file_upload']->getMimeType(), [
+ 'application/zip',
+ 'text/plain',
+ 'application/json',
+ ])) {
+ throw new DisplayException('The file provided (' . $data['file_upload']->getMimeType() . ') does not meet the required filetypes of application/zip or application/json.');
+ }
+
+ if ($data['file_upload']->getMimeType() === 'application/zip') {
+ $zip = new \ZipArchive;
+ if (! $zip->open($data['file_upload']->path())) {
+ throw new DisplayException('The uploaded archive was unable to be opened.');
+ }
+
+ $isZip = $zip->locateName('archive.zip');
+ $isTar = $zip->locateName('archive.tar.gz');
+
+ if ($zip->locateName('import.json') === false || ($isZip === false && $isTar === false)) {
+ throw new DisplayException('This contents of the provided archive were in an invalid format.');
+ }
+
+ $json = json_decode($zip->getFromName('import.json'));
+ $id = $this->create([
+ 'name' => $json->name,
+ 'version' => $json->version,
+ 'description' => $json->description,
+ 'option' => $data['option'],
+ 'selectable' => $json->selectable,
+ 'visible' => $json->visible,
+ 'build_memory' => $json->build->memory,
+ 'build_swap' => $json->build->swap,
+ 'build_cpu' => $json->build->cpu,
+ 'build_io' => $json->build->io,
+ 'build_container' => $json->build->container,
+ 'build_script' => $json->build->script,
+ ]);
+
+ $pack = Models\ServicePack::findOrFail($id);
+ if (! $zip->extractTo(storage_path('app/packs/' . $pack->uuid), ($isZip === false) ? 'archive.tar.gz' : 'archive.zip')) {
+ $pack->delete();
+ throw new DisplayException('Unable to extract the archive file to the correct location.');
+ }
+
+ $zip->close();
+
+ return $pack->id;
+ } else {
+ $json = json_decode(file_get_contents($data['file_upload']->path()));
+
+ return $this->create([
+ 'name' => $json->name,
+ 'version' => $json->version,
+ 'description' => $json->description,
+ 'option' => $data['option'],
+ 'selectable' => $json->selectable,
+ 'visible' => $json->visible,
+ 'build_memory' => $json->build->memory,
+ 'build_swap' => $json->build->swap,
+ 'build_cpu' => $json->build->cpu,
+ 'build_io' => $json->build->io,
+ 'build_container' => $json->build->container,
+ 'build_script' => $json->build->script,
+ ]);
+ }
+ }
+
+ public function update($id, array $data)
+ {
+ $validator = Validator::make($data, [
+ 'name' => 'required|string',
+ 'version' => 'required|string',
+ 'description' => 'string',
+ 'option' => 'required|exists:service_options,id',
+ 'selectable' => 'sometimes|boolean',
+ 'visible' => 'sometimes|boolean',
+ 'build_memory' => 'required|integer|min:0',
+ 'build_swap' => 'required|integer|min:0',
+ 'build_cpu' => 'required|integer|min:0',
+ 'build_io' => 'required|integer|min:10|max:1000',
+ 'build_container' => 'required|string',
+ 'build_script' => 'sometimes|string',
+ ]);
+
+ if ($validator->fails()) {
+ throw new DisplayValidationException($validator->errors());
+ }
+
+ DB::transaction(function () use ($id, $data) {
+ Models\ServicePack::findOrFail($id)->update([
+ 'option' => $data['option'],
+ 'build_memory' => $data['build_memory'],
+ 'build_swap' => $data['build_swap'],
+ 'build_cpu' => $data['build_swap'],
+ 'build_io' => $data['build_io'],
+ 'build_script' => (empty($data['build_script'])) ? null : $data['build_script'],
+ 'build_container' => $data['build_container'],
+ 'name' => $data['name'],
+ 'version' => $data['version'],
+ 'description' => (empty($data['description'])) ? null : $data['description'],
+ 'selectable' => isset($data['selectable']),
+ 'visible' => isset($data['visible']),
+ ]);
+
+ return true;
+ });
+ }
+
+ public function delete($id)
+ {
+ $pack = Models\ServicePack::findOrFail($id);
+ // @TODO Check for linked servers; foreign key should block this.
+ DB::transaction(function () use ($pack) {
+ $pack->delete();
+ Storage::deleteDirectory('packs/' . $pack->uuid);
+ });
+ }
+}
diff --git a/app/Repositories/ServiceRepository/Service.php b/app/Repositories/ServiceRepository/Service.php
index 565e52468..becc290e6 100644
--- a/app/Repositories/ServiceRepository/Service.php
+++ b/app/Repositories/ServiceRepository/Service.php
@@ -26,6 +26,7 @@ namespace Pterodactyl\Repositories\ServiceRepository;
use DB;
use Uuid;
+use Storage;
use Validator;
use Pterodactyl\Models;
use Pterodactyl\Exceptions\DisplayException;
@@ -43,7 +44,7 @@ class Service
$validator = Validator::make($data, [
'name' => 'required|string|min:1|max:255',
'description' => 'required|string',
- 'file' => 'required|regex:/^[\w.-]{1,50}$/',
+ 'file' => 'required|unique:services,file|regex:/^[\w.-]{1,50}$/',
'executable' => 'max:255|regex:/^(.*)$/',
'startup' => 'string',
]);
@@ -52,15 +53,23 @@ class Service
throw new DisplayValidationException($validator->errors());
}
- if (Models\Service::where('file', $data['file'])->first()) {
- throw new DisplayException('A service using that configuration file already exists on the system.');
- }
-
$data['author'] = env('SERVICE_AUTHOR', (string) Uuid::generate(4));
$service = new Models\Service;
- $service->fill($data);
- $service->save();
+ DB::beginTransaction();
+
+ try {
+ $service->fill($data);
+ $service->save();
+
+ Storage::put('services/' . $data['file'] . '/main.json', '{}');
+ Storage::copy('services/.templates/index.js', 'services/' . $data['file'] . '/index.js');
+
+ DB::commit();
+ } catch (\Exception $ex) {
+ DB::rollBack();
+ throw $ex;
+ }
return $service->id;
}
@@ -101,10 +110,38 @@ class Service
Models\ServiceVariables::whereIn('option_id', $options->get()->toArray())->delete();
$options->delete();
$service->delete();
+
+ Storage::deleteDirectory('services/' . $service->file);
DB::commit();
} catch (\Exception $ex) {
DB::rollBack();
throw $ex;
}
}
+
+ public function updateFile($id, array $data)
+ {
+ $service = Models\Service::findOrFail($id);
+
+ $validator = Validator::make($data, [
+ 'file' => 'required|in:index,main',
+ 'contents' => 'required|string',
+ ]);
+
+ if ($validator->fails()) {
+ throw new DisplayValidationException($validator->errors());
+ }
+
+ $filename = ($data['file'] === 'main') ? 'main.json' : 'index.js';
+ $filepath = 'services/' . $service->file . '/' . $filename;
+ $backup = 'services/.bak/' . str_random(12) . '.bak';
+
+ try {
+ Storage::move($filepath, $backup);
+ Storage::put($filepath, $data['contents']);
+ } catch (\Exception $ex) {
+ Storage::move($backup, $filepath);
+ throw $ex;
+ }
+ }
}
diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php
index add04c920..db715fbbc 100644
--- a/app/Repositories/UserRepository.php
+++ b/app/Repositories/UserRepository.php
@@ -29,6 +29,7 @@ use DB;
use Auth;
use Hash;
use Carbon;
+use Settings;
use Validator;
use Pterodactyl\Models;
use Pterodactyl\Services\UuidService;
@@ -52,18 +53,16 @@ class UserRepository
* @param int $token A custom user ID.
* @return bool|int
*/
- public function create($email, $password = null, $admin = false, $token = null)
+ public function create(array $data)
{
- $validator = Validator::make([
- 'email' => $email,
- 'password' => $password,
- 'root_admin' => $admin,
- 'custom_id' => $token,
- ], [
+ $validator = Validator::make($data, [
'email' => 'required|email|unique:users,email',
- 'password' => 'nullable|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})',
+ 'username' => 'required|string|between:1,255|unique:users,username|' . Models\User::USERNAME_RULES,
+ 'name_first' => 'required|string|between:1,255',
+ 'name_last' => 'required|string|between:1,255',
+ 'password' => 'sometimes|nullable|' . Models\User::PASSWORD_RULES,
'root_admin' => 'required|boolean',
- 'custom_id' => 'nullable|unique:users,id',
+ 'custom_id' => 'sometimes|nullable|unique:users,id',
]);
// Run validator, throw catchable and displayable exception if it fails.
@@ -79,26 +78,36 @@ class UserRepository
$uuid = new UuidService;
// Support for API Services
- if (! is_null($token)) {
+ if (isset($data['custom_id']) && ! is_null($data['custom_id'])) {
$user->id = $token;
}
+ // UUIDs are not mass-fillable.
$user->uuid = $uuid->generate('users', 'uuid');
- $user->email = $email;
- $user->password = Hash::make((is_null($password)) ? str_random(30) : $password);
- $user->language = 'en';
- $user->root_admin = ($admin) ? 1 : 0;
+
+ $user->fill([
+ 'email' => $data['email'],
+ 'username' => $data['username'],
+ 'name_first' => $data['name_first'],
+ 'name_last' => $data['name_last'],
+ 'password' => Hash::make((empty($data['password'])) ? str_random(30) : $password),
+ 'root_admin' => $data['root_admin'],
+ 'language' => Settings::get('default_language', 'en'),
+ ]);
$user->save();
// Setup a Password Reset to use when they set a password.
- $token = str_random(32);
- DB::table('password_resets')->insert([
- 'email' => $user->email,
- 'token' => $token,
- 'created_at' => Carbon::now()->toDateTimeString(),
- ]);
+ // Only used if no password is provided.
+ if (empty($data['password'])) {
+ $token = str_random(32);
+ DB::table('password_resets')->insert([
+ 'email' => $user->email,
+ 'token' => $token,
+ 'created_at' => Carbon::now()->toDateTimeString(),
+ ]);
- $user->notify((new AccountCreated($token)));
+ $user->notify((new AccountCreated($token)));
+ }
DB::commit();
@@ -122,7 +131,10 @@ class UserRepository
$validator = Validator::make($data, [
'email' => 'sometimes|required|email|unique:users,email,' . $id,
- 'password' => 'sometimes|required|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})',
+ 'username' => 'sometimes|required|string|between:1,255|unique:users,username,' . $user->id . '|' . Models\User::USERNAME_RULES,
+ 'name_first' => 'sometimes|required|string|between:1,255',
+ 'name_last' => 'sometimes|required|string|between:1,255',
+ 'password' => 'sometimes|nullable|' . Models\User::PASSWORD_RULES,
'root_admin' => 'sometimes|required|boolean',
'language' => 'sometimes|required|string|min:1|max:5',
'use_totp' => 'sometimes|required|boolean',
@@ -135,12 +147,15 @@ class UserRepository
throw new DisplayValidationException($validator->errors());
}
- if (array_key_exists('password', $data)) {
+ // The password and root_admin fields are not mass assignable.
+ if (! empty($data['password'])) {
$data['password'] = Hash::make($data['password']);
+ } else {
+ unset($data['password']);
}
- if (isset($data['password_confirmation'])) {
- unset($data['password_confirmation']);
+ if (! empty($data['root_admin'])) {
+ $user->root_admin = $data['root_admin'];
}
$user->fill($data);
diff --git a/database/migrations/2016_11_11_220649_add_pack_support.php b/database/migrations/2016_11_11_220649_add_pack_support.php
new file mode 100644
index 000000000..87a66b40a
--- /dev/null
+++ b/database/migrations/2016_11_11_220649_add_pack_support.php
@@ -0,0 +1,46 @@
+increments('id');
+ $table->unsignedInteger('option');
+ $table->char('uuid', 36)->unique();
+ $table->unsignedInteger('build_memory')->nullable();
+ $table->unsignedInteger('build_swap')->nullable();
+ $table->unsignedInteger('build_cpu')->nullable();
+ $table->unsignedInteger('build_io')->nullable();
+ $table->text('build_script')->nullable();
+ $table->string('build_container')->default('alpine:latest');
+ $table->string('name');
+ $table->string('version');
+ $table->text('description')->nullable();
+ $table->boolean('selectable')->default(true);
+ $table->boolean('visible')->default(true);
+ $table->timestamps();
+
+ $table->foreign('option')->references('id')->on('service_options');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::drop('service_packs');
+ }
+}
diff --git a/database/migrations/2016_11_11_231731_set_service_name_unique.php b/database/migrations/2016_11_11_231731_set_service_name_unique.php
new file mode 100644
index 000000000..4db76f8e8
--- /dev/null
+++ b/database/migrations/2016_11_11_231731_set_service_name_unique.php
@@ -0,0 +1,32 @@
+unique('name');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('services', function (Blueprint $table) {
+ $table->dropUnique('services_name_unique');
+ });
+ }
+}
diff --git a/database/migrations/2016_11_27_142519_add_pack_column.php b/database/migrations/2016_11_27_142519_add_pack_column.php
new file mode 100644
index 000000000..f2c2f0964
--- /dev/null
+++ b/database/migrations/2016_11_27_142519_add_pack_column.php
@@ -0,0 +1,36 @@
+unsignedInteger('pack')->nullable()->after('option');
+
+ $table->foreign('pack')->references('id')->on('service_packs');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dropForeign('servers_pack_foreign');
+ $table->dropIndex('servers_pack_foreign');
+ $table->dropColumn('pack');
+ });
+ }
+}
diff --git a/database/migrations/2017_01_12_135449_add_more_user_data.php b/database/migrations/2017_01_12_135449_add_more_user_data.php
new file mode 100644
index 000000000..67bc3f59d
--- /dev/null
+++ b/database/migrations/2017_01_12_135449_add_more_user_data.php
@@ -0,0 +1,50 @@
+string('name_first')->after('email')->nullable();
+ $table->string('name_last')->after('name_first')->nullable();
+ $table->string('username')->after('uuid');
+ $table->boolean('gravatar')->after('totp_secret')->default(true);
+ });
+
+ DB::transaction(function () {
+ foreach (User::all() as &$user) {
+ $user->username = $user->email;
+ $user->save();
+ }
+ });
+
+ Schema::table('users', function (Blueprint $table) {
+ $table->string('username')->unique()->change();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('name_first');
+ $table->dropColumn('name_last');
+ $table->dropColumn('username');
+ $table->dropColumn('gravatar');
+ });
+ }
+}
diff --git a/public/themes/default/css/pterodactyl.css b/public/themes/default/css/pterodactyl.css
index 739f65fb1..f6e37f4e7 100755
--- a/public/themes/default/css/pterodactyl.css
+++ b/public/themes/default/css/pterodactyl.css
@@ -325,3 +325,14 @@ td.has-progress {
padding:0;
border:0;
}
+
+.fuelux .checkbox-formheight.checkbox-custom.checkbox-inline.highlight {
+ height: 36px;
+ padding: 10px 8px 4px 28px;
+ width: 100%;
+}
+
+.fuelux .checkbox-formheight.checkbox-custom.checkbox-inline.highlight:before {
+ left: 8px;
+ top: 11px;
+}
diff --git a/resources/views/admin/servers/new.blade.php b/resources/views/admin/servers/new.blade.php
index 12defcd97..5eda226f2 100644
--- a/resources/views/admin/servers/new.blade.php
+++ b/resources/views/admin/servers/new.blade.php
@@ -201,6 +201,15 @@
Select the type of service that this server will be running.
+
+
+
+
+
Select the service pack that should be used for this server. This option can be changed later.
+
+
@@ -392,6 +401,7 @@ $(document).ready(function () {
handleLoader('#load_services', true);
$('#serviceOptions').slideUp();
$('#getOption').html('');
+ $('#getPack').html('');
$.ajax({
method: 'POST',
@@ -423,10 +433,11 @@ $(document).ready(function () {
handleLoader('#serviceOptions', true);
$('#serverVariables').html('');
$('input[name="custom_image_name"]').val($(this).find(':selected').data('image'));
+ $('#getPack').html('');
$.ajax({
method: 'POST',
- url: '/admin/servers/new/service-variables',
+ url: '/admin/servers/new/option-details',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
@@ -436,6 +447,12 @@ $(document).ready(function () {
}).done(function (data) {
$('#startupExec').html(data.exec);
$('input[name="startup"]').val(data.startup);
+
+ $.each(data.packs, function (i, item) {
+ $('#getPack').append('');
+ });
+ $('#getPack').append('').parent().parent().removeClass('hidden');
+
$.each(data.variables, function (i, item) {
var isRequired = (item.required === 1) ? 'Required ' : '';
var dataAppend = ' \
diff --git a/resources/views/admin/services/config.blade.php b/resources/views/admin/services/config.blade.php
new file mode 100644
index 000000000..fbfa01d38
--- /dev/null
+++ b/resources/views/admin/services/config.blade.php
@@ -0,0 +1,180 @@
+{{-- Copyright (c) 2015 - 2016 Dane Everitt --}}
+
+{{-- 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')
+ Manage Service Configuration
+@endsection
+
+@section('content')
+
This should be the name of the folder on the daemon that contains all of the service logic.
+
This should be a unique alpha-numeric (a-z) name used to identify the service.
diff --git a/resources/views/admin/services/packs/byoption.blade.php b/resources/views/admin/services/packs/byoption.blade.php
new file mode 100644
index 000000000..626958566
--- /dev/null
+++ b/resources/views/admin/services/packs/byoption.blade.php
@@ -0,0 +1,90 @@
+{{-- Copyright (c) 2015 - 2016 Dane Everitt --}}
+
+{{-- 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 Packs for {{ $option->name }}
+@endsection
+
+@section('content')
+
+
+@endsection
diff --git a/resources/views/admin/services/packs/byservice.blade.php b/resources/views/admin/services/packs/byservice.blade.php
new file mode 100644
index 000000000..f73572c33
--- /dev/null
+++ b/resources/views/admin/services/packs/byservice.blade.php
@@ -0,0 +1,67 @@
+{{-- Copyright (c) 2015 - 2016 Dane Everitt --}}
+
+{{-- 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 Packs for {{ $service->name }}
+@endsection
+
+@section('content')
+
+
+@endsection
diff --git a/resources/views/admin/services/packs/edit.blade.php b/resources/views/admin/services/packs/edit.blade.php
new file mode 100644
index 000000000..b71256fd9
--- /dev/null
+++ b/resources/views/admin/services/packs/edit.blade.php
@@ -0,0 +1,218 @@
+{{-- Copyright (c) 2015 - 2016 Dane Everitt --}}
+
+{{-- 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')
+ Add New Service Pack
+@endsection
+
+@section('content')
+