diff --git a/app/Contracts/Repository/UserRepositoryInterface.php b/app/Contracts/Repository/UserRepositoryInterface.php index 0265d6f44..b35914c04 100644 --- a/app/Contracts/Repository/UserRepositoryInterface.php +++ b/app/Contracts/Repository/UserRepositoryInterface.php @@ -35,16 +35,6 @@ interface UserRepositoryInterface extends RepositoryInterface, SearchableInterfa */ public function getAllUsersWithCounts(); - /** - * Delete a user if they have no servers attached to their account. - * - * @param int $id - * @return bool - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function deleteIfNoServers($id); - /** * Return all matching models for a user in a format that can be used for dropdowns. * diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 42da3f57f..81a4a7783 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -25,13 +25,16 @@ namespace Pterodactyl\Http\Controllers\Admin; use Illuminate\Http\Request; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Services\UserService; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Users\UpdateService; +use Pterodactyl\Services\Users\CreationService; +use Pterodactyl\Services\Users\DeletionService; +use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Http\Requests\Admin\UserFormRequest; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class UserController extends Controller { @@ -41,53 +44,67 @@ class UserController extends Controller protected $alert; /** - * @var \Pterodactyl\Services\UserService + * @var \Pterodactyl\Services\Users\CreationService */ - protected $service; + protected $creationService; /** - * @var \Pterodactyl\Models\User + * @var \Pterodactyl\Services\Users\DeletionService */ - protected $model; + protected $deletionService; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ protected $repository; + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * @var \Pterodactyl\Services\Users\UpdateService + */ + protected $updateService; + /** * UserController constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\UserService $service + * @param \Pterodactyl\Services\Users\CreationService $creationService + * @param \Pterodactyl\Services\Users\DeletionService $deletionService + * @param \Illuminate\Contracts\Translation\Translator $translator + * @param \Pterodactyl\Services\Users\UpdateService $updateService * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository - * @param \Pterodactyl\Models\User $model */ public function __construct( AlertsMessageBag $alert, - UserService $service, - UserRepositoryInterface $repository, - User $model + CreationService $creationService, + DeletionService $deletionService, + Translator $translator, + UpdateService $updateService, + UserRepositoryInterface $repository ) { $this->alert = $alert; - $this->service = $service; - $this->model = $model; + $this->creationService = $creationService; + $this->deletionService = $deletionService; $this->repository = $repository; + $this->translator = $translator; + $this->updateService = $updateService; } /** * Display user index page. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) { $users = $this->repository->search($request->input('query'))->getAllUsersWithCounts(); - return view('admin.users.index', [ - 'users' => $users, - ]); + return view('admin.users.index', ['users' => $users]); } /** @@ -103,21 +120,19 @@ class UserController extends Controller /** * Display user view page. * - * @param \Pterodactyl\Models\User $user + * @param \Pterodactyl\Models\User $user * @return \Illuminate\View\View */ public function view(User $user) { - return view('admin.users.view', [ - 'user' => $user, - ]); + return view('admin.users.view', ['user' => $user]); } /** * Delete a user from the system. * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\User $user + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\User $user * @return \Illuminate\Http\RedirectResponse * * @throws \Exception @@ -126,16 +141,10 @@ class UserController extends Controller public function delete(Request $request, User $user) { if ($request->user()->id === $user->id) { - throw new DisplayException('Cannot delete your own account.'); + throw new DisplayException($this->translator->trans('admin/user.exceptions.user_has_servers')); } - try { - $this->repository->deleteIfNoServers($user->id); - - return redirect()->route('admin.users'); - } catch (DisplayException $ex) { - $this->alert->danger($ex->getMessage())->flash(); - } + $this->deletionService->handle($user); return redirect()->route('admin.users.view', $user->id); } @@ -143,7 +152,7 @@ class UserController extends Controller /** * Create a user. * - * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Exception @@ -151,9 +160,8 @@ class UserController extends Controller */ public function store(UserFormRequest $request) { - $user = $this->service->create($request->normalize()); - - $this->alert->success('Account has been successfully created.')->flash(); + $user = $this->creationService->handle($request->normalize()); + $this->alert->success($this->translator->trans('admin/user.notices.account_created'))->flash(); return redirect()->route('admin.users.view', $user->id); } @@ -169,8 +177,8 @@ class UserController extends Controller */ public function update(UserFormRequest $request, User $user) { - $this->service->update($user->id, $request->normalize()); - $this->alert->success('User account has been updated.')->flash(); + $this->updateService->handle($user->id, $request->normalize()); + $this->alert->success($this->translator->trans('admin/user.notices.account_updated'))->flash(); return redirect()->route('admin.users.view', $user->id); } diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php index 8915b33a7..633b92fd0 100644 --- a/app/Repositories/Eloquent/UserRepository.php +++ b/app/Repositories/Eloquent/UserRepository.php @@ -27,8 +27,6 @@ namespace Pterodactyl\Repositories\Eloquent; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Foundation\Application; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\User; use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; @@ -76,24 +74,6 @@ class UserRepository extends SearchableRepository implements UserRepositoryInter ); } - /** - * {@inheritdoc} - */ - public function deleteIfNoServers($id) - { - $user = $this->getBuilder()->withCount('servers')->where('id', $id)->first(); - - if (! $user) { - throw new RecordNotFoundException(); - } - - if ($user->servers_count > 0) { - throw new DisplayException('Cannot delete an account that has active servers attached to it.'); - } - - return $user->delete(); - } - /** * {@inheritdoc} */ diff --git a/app/Repositories/Old/old_ServerRepository.php b/app/Repositories/Old/old_ServerRepository.php deleted file mode 100644 index 8cfe9ca47..000000000 --- a/app/Repositories/Old/old_ServerRepository.php +++ /dev/null @@ -1,1036 +0,0 @@ -. - * - * 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; - -use DB; -use Crypt; -use Validator; -use Pterodactyl\Models\Node; -use Pterodactyl\Models\Pack; -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Service; -use Pterodactyl\Models\Allocation; -use Pterodactyl\Models\ServiceOption; -use Pterodactyl\Services\UuidService; -use Pterodactyl\Models\ServerVariable; -use Pterodactyl\Models\ServiceVariable; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\TransferException; -use Pterodactyl\Services\DeploymentService; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class old_ServerRepository -{ - /** - * An array of daemon permission to assign to this server. - * - * @var array - */ - protected $daemonPermissions = [ - 's:*', - ]; - - /** - * Generates a SFTP username for a server given a server name. - * format: mumble_67c7a4b0. - * - * @param string $name - * @param null|string $identifier - * @return string - */ - protected function generateSFTPUsername($name, $identifier = null) - { - if (is_null($identifier) || ! ctype_alnum($identifier)) { - $unique = str_random(8); - } else { - if (strlen($identifier) < 8) { - $unique = $identifier . str_random((8 - strlen($identifier))); - } else { - $unique = substr($identifier, 0, 8); - } - } - - // Filter the Server Name - $name = trim(preg_replace('/[^\w]+/', '', $name), '_'); - $name = (strlen($name) < 1) ? str_random(6) : $name; - - return strtolower(substr($name, 0, 6) . '_' . $unique); - } - - /** - * Adds a new server to the system. - * - * @param array $data - * @return \Pterodactyl\Models\Server - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\AutoDeploymentException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'user_id' => 'required|exists:users,id', - 'name' => 'required|regex:/^([\w .-]{1,200})$/', - 'description' => 'sometimes|nullable|string', - 'memory' => 'required|numeric|min:0', - 'swap' => 'required|numeric|min:-1', - 'io' => 'required|numeric|min:10|max:1000', - 'cpu' => 'required|numeric|min:0', - 'disk' => 'required|numeric|min:0', - 'service_id' => 'required|numeric|min:1|exists:services,id', - 'option_id' => 'required|numeric|min:1|exists:service_options,id', - 'location_id' => 'required|numeric|min:1|exists:locations,id', - 'pack_id' => 'sometimes|nullable|numeric|min:0', - 'custom_container' => 'string', - 'startup' => 'string', - 'auto_deploy' => 'sometimes|required|accepted', - 'custom_id' => 'sometimes|required|numeric|unique:servers,id', - 'skip_scripts' => 'sometimes|required|boolean', - ]); - - $validator->sometimes('node_id', 'required|numeric|min:1|exists:nodes,id', function ($input) { - return ! ($input->auto_deploy); - }); - - $validator->sometimes('allocation_id', 'required|numeric|exists:allocations,id', function ($input) { - return ! ($input->auto_deploy); - }); - - $validator->sometimes('allocation_additional.*', 'sometimes|required|numeric|exists:allocations,id', function ($input) { - return ! ($input->auto_deploy); - }); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - $user = User::findOrFail($data['user_id']); - - $deployment = false; - if (isset($data['auto_deploy'])) { - $deployment = new DeploymentService; - - if (isset($data['location_id'])) { - $deployment->setLocation($data['location_id']); - } - - $deployment->setMemory($data['memory'])->setDisk($data['disk'])->select(); - } - - $node = (! $deployment) ? Node::findOrFail($data['node_id']) : $deployment->node(); - - // Verify IP & Port are a.) free and b.) assigned to the node. - // We know the node exists because of 'exists:nodes,id' in the validation - if (! $deployment) { - $allocation = Allocation::where('id', $data['allocation_id'])->where('node_id', $data['node_id'])->whereNull('server_id')->first(); - } else { - $allocation = $deployment->allocation(); - } - - // Something failed in the query, either that combo doesn't exist, or it is in use. - if (! $allocation) { - throw new DisplayException('The selected Allocation ID is either already in use, or unavaliable for this node.'); - } - - // Validate those Service Option Variables - // We know the service and option exists because of the validation. - // We need to verify that the option exists for the service, and then check for - // any required variable fields. (fields are labeled env_) - $option = ServiceOption::where('id', $data['option_id'])->where('service_id', $data['service_id'])->first(); - if (! $option) { - throw new DisplayException('The requested service option does not exist for the specified service.'); - } - - // Validate the Pack - if (! isset($data['pack_id']) || (int) $data['pack_id'] < 1) { - $data['pack_id'] = null; - } else { - $pack = Pack::where('id', $data['pack_id'])->where('option_id', $data['option_id'])->first(); - if (! $pack) { - throw new DisplayException('The requested service pack does not seem to exist for this combination.'); - } - } - - // Load up the Service Information - $service = Service::find($option->service_id); - - // Check those Variables - $variables = ServiceVariable::where('option_id', $data['option_id'])->get(); - $variableList = []; - if ($variables) { - foreach ($variables as $variable) { - - // Is the variable required? - if (! isset($data['env_' . $variable->env_variable])) { - if ($variable->required) { - throw new DisplayException('A required service option variable field (env_' . $variable->env_variable . ') was missing from the request.'); - } - $variableList[] = [ - 'id' => $variable->id, - 'env' => $variable->env_variable, - 'val' => $variable->default_value, - ]; - continue; - } - - // Check aganist Regex Pattern - if (! is_null($variable->regex) && ! preg_match($variable->regex, $data['env_' . $variable->env_variable])) { - throw new DisplayException('Failed to validate service option variable field (env_' . $variable->env_variable . ') aganist regex (' . $variable->regex . ').'); - } - - $variableList[] = [ - 'id' => $variable->id, - 'env' => $variable->env_variable, - 'val' => $data['env_' . $variable->env_variable], - ]; - continue; - } - } - - // Check Overallocation - if (! $deployment) { - if (is_numeric($node->memory_overallocate) || is_numeric($node->disk_overallocate)) { - $totals = Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node_id', $node->id)->first(); - - // Check memory limits - if (is_numeric($node->memory_overallocate)) { - $newMemory = $totals->memory + $data['memory']; - $memoryLimit = ($node->memory * (1 + ($node->memory_overallocate / 100))); - if ($newMemory > $memoryLimit) { - throw new DisplayException('The amount of memory allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->memory_overallocate + 100) . '% of its assigned ' . $node->memory . 'Mb of memory (' . $memoryLimit . 'Mb) of which ' . (($totals->memory / $node->memory) * 100) . '% (' . $totals->memory . 'Mb) is in use already. By allocating this server the node would be at ' . (($newMemory / $node->memory) * 100) . '% (' . $newMemory . 'Mb) usage.'); - } - } - - // Check Disk Limits - if (is_numeric($node->disk_overallocate)) { - $newDisk = $totals->disk + $data['disk']; - $diskLimit = ($node->disk * (1 + ($node->disk_overallocate / 100))); - if ($newDisk > $diskLimit) { - throw new DisplayException('The amount of disk allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->disk_overallocate + 100) . '% of its assigned ' . $node->disk . 'Mb of disk (' . $diskLimit . 'Mb) of which ' . (($totals->disk / $node->disk) * 100) . '% (' . $totals->disk . 'Mb) is in use already. By allocating this server the node would be at ' . (($newDisk / $node->disk) * 100) . '% (' . $newDisk . 'Mb) usage.'); - } - } - } - } - - DB::beginTransaction(); - - try { - $uuid = new UuidService; - - // Add Server to the Database - $server = new Server; - $genUuid = $uuid->generate('servers', 'uuid'); - $genShortUuid = $uuid->generateShort('servers', 'uuidShort', $genUuid); - - if (isset($data['custom_id'])) { - $server->id = $data['custom_id']; - } - - $server->fill([ - 'uuid' => $genUuid, - 'uuidShort' => $genShortUuid, - 'node_id' => $node->id, - 'name' => $data['name'], - 'description' => $data['description'], - 'skip_scripts' => isset($data['skip_scripts']), - 'suspended' => false, - 'owner_id' => $user->id, - 'memory' => $data['memory'], - 'swap' => $data['swap'], - 'disk' => $data['disk'], - 'io' => $data['io'], - 'cpu' => $data['cpu'], - 'oom_disabled' => isset($data['oom_disabled']), - 'allocation_id' => $allocation->id, - 'service_id' => $data['service_id'], - 'option_id' => $data['option_id'], - 'pack_id' => $data['pack_id'], - 'startup' => $data['startup'], - 'daemonSecret' => $uuid->generate('servers', 'daemonSecret'), - 'image' => (isset($data['custom_container']) && ! empty($data['custom_container'])) ? $data['custom_container'] : $option->docker_image, - 'username' => $this->generateSFTPUsername($data['name'], $genShortUuid), - 'sftp_password' => Crypt::encrypt('not set'), - ]); - $server->save(); - - // Mark Allocation in Use - $allocation->server_id = $server->id; - $allocation->save(); - - // Add Additional Allocations - if (isset($data['allocation_additional']) && is_array($data['allocation_additional'])) { - foreach ($data['allocation_additional'] as $allocation) { - $model = Allocation::where('id', $allocation)->where('node_id', $data['node_id'])->whereNull('server_id')->first(); - if (! $model) { - continue; - } - - $model->server_id = $server->id; - $model->save(); - } - } - - foreach ($variableList as $item) { - ServerVariable::create([ - 'server_id' => $server->id, - 'variable_id' => $item['id'], - 'variable_value' => $item['val'], - ]); - } - - $environment = $this->parseVariables($server); - $server->load('allocation', 'allocations'); - - $node->guzzleClient(['X-Access-Token' => $node->daemonSecret])->request('POST', '/servers', [ - 'json' => [ - 'uuid' => (string) $server->uuid, - 'user' => $server->username, - 'build' => [ - 'default' => [ - 'ip' => $server->allocation->ip, - 'port' => $server->allocation->port, - ], - 'ports' => $server->allocations->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(), - 'env' => $environment->pluck('value', 'variable')->toArray(), - 'memory' => (int) $server->memory, - 'swap' => (int) $server->swap, - 'io' => (int) $server->io, - 'cpu' => (int) $server->cpu, - 'disk' => (int) $server->disk, - 'image' => $server->image, - ], - 'service' => [ - 'type' => $service->folder, - 'option' => $option->tag, - 'pack' => (isset($pack)) ? $pack->uuid : null, - 'skip_scripts' => $server->skip_scripts, - ], - 'keys' => [ - (string) $server->daemonSecret => $this->daemonPermissions, - ], - 'rebuild' => false, - 'start_on_completion' => isset($data['start_on_completion']), - ], - ]); - - DB::commit(); - - return $server; - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Update the details for a server. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Server - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function updateDetails($id, array $data) - { - $uuid = new UuidService; - $resetDaemonKey = false; - - // Validate Fields - $validator = Validator::make($data, [ - 'owner_id' => 'sometimes|required|integer|exists:users,id', - 'name' => 'sometimes|required|regex:([\w .-]{1,200})', - 'description' => 'sometimes|nullable|string', - 'reset_token' => 'sometimes|required|accepted', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - - try { - $server = Server::with('user')->findOrFail($id); - - // Update daemon secret if it was passed. - if (isset($data['reset_token']) || (isset($data['owner_id']) && (int) $data['owner_id'] !== $server->user->id)) { - $oldDaemonKey = $server->daemonSecret; - $server->daemonSecret = $uuid->generate('servers', 'daemonSecret'); - $resetDaemonKey = true; - } - - // Save our changes - $server->fill($data)->save(); - - // Do we need to update? If not, return successful. - if (! $resetDaemonKey) { - return DB::commit(); - } - - $res = $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'exceptions' => false, - 'json' => [ - 'keys' => [ - (string) $oldDaemonKey => [], - (string) $server->daemonSecret => $this->daemonPermissions, - ], - ], - ]); - - if ($res->getStatusCode() === 204) { - DB::commit(); - - return $server; - } else { - throw new DisplayException('Daemon returned a a non HTTP/204 error code. HTTP/' + $res->getStatusCode()); - } - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Update the container for a server. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Server - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function updateContainer($id, array $data) - { - $validator = Validator::make($data, [ - 'docker_image' => 'required|string', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - try { - $server = Server::findOrFail($id); - - $server->image = $data['docker_image']; - $server->save(); - - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'build' => [ - 'image' => $server->image, - ], - ], - ]); - - DB::commit(); - - return $server; - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Update the build details for a server. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Server - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function changeBuild($id, array $data) - { - $validator = Validator::make($data, [ - 'allocation_id' => 'sometimes|required|exists:allocations,id', - 'add_allocations' => 'sometimes|required|array', - 'remove_allocations' => 'sometimes|required|array', - 'memory' => 'sometimes|required|integer|min:0', - 'swap' => 'sometimes|required|integer|min:-1', - 'io' => 'sometimes|required|integer|min:10|max:1000', - 'cpu' => 'sometimes|required|integer|min:0', - 'disk' => 'sometimes|required|integer|min:0', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - - try { - $server = Server::with('allocation', 'allocations')->findOrFail($id); - $newBuild = []; - $newAllocations = []; - - if (isset($data['allocation_id'])) { - if ((int) $data['allocation_id'] !== $server->allocation_id) { - $selection = $server->allocations->where('id', $data['allocation_id'])->first(); - if (! $selection) { - throw new DisplayException('The requested default connection is not allocated to this server.'); - } - - $server->allocation_id = $selection->id; - $newBuild['default'] = ['ip' => $selection->ip, 'port' => $selection->port]; - - $server->load('allocation'); - } - } - - $newPorts = false; - $firstNewAllocation = null; - // Add Assignments - if (isset($data['add_allocations'])) { - foreach ($data['add_allocations'] as $allocation) { - $model = Allocation::where('id', $allocation)->whereNull('server_id')->first(); - if (! $model) { - continue; - } - - $newPorts = true; - $firstNewAllocation = $firstNewAllocation ?? $model; - $model->update([ - 'server_id' => $server->id, - ]); - } - - $server->load('allocations'); - } - - // Remove Assignments - if (isset($data['remove_allocations'])) { - foreach ($data['remove_allocations'] as $allocation) { - // Can't remove the assigned IP/Port combo - if ((int) $allocation === $server->allocation_id) { - // No New Allocation - if (is_null($firstNewAllocation)) { - continue; - } - - // New Allocation, set as the default. - $server->allocation_id = $firstNewAllocation->id; - $newBuild['default'] = ['ip' => $firstNewAllocation->ip, 'port' => $firstNewAllocation->port]; - } - - $newPorts = true; - Allocation::where('id', $allocation)->where('server_id', $server->id)->update([ - 'server_id' => null, - ]); - } - - $server->load('allocations'); - } - - if ($newPorts) { - $newBuild['ports|overwrite'] = $server->allocations->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(); - - $newBuild['env|overwrite'] = $this->parseVariables($server)->pluck('value', 'variable')->toArray(); - } - - // @TODO: verify that server can be set to this much memory without - // going over node limits. - if (isset($data['memory']) && $server->memory !== (int) $data['memory']) { - $server->memory = $data['memory']; - $newBuild['memory'] = (int) $server->memory; - } - - if (isset($data['swap']) && $server->swap !== (int) $data['swap']) { - $server->swap = $data['swap']; - $newBuild['swap'] = (int) $server->swap; - } - - // @TODO: verify that server can be set to this much disk without - // going over node limits. - if (isset($data['disk']) && $server->disk !== (int) $data['disk']) { - $server->disk = $data['disk']; - $newBuild['disk'] = (int) $server->disk; - } - - if (isset($data['cpu']) && $server->cpu !== (int) $data['cpu']) { - $server->cpu = $data['cpu']; - $newBuild['cpu'] = (int) $server->cpu; - } - - if (isset($data['io']) && $server->io !== (int) $data['io']) { - $server->io = $data['io']; - $newBuild['io'] = (int) $server->io; - } - - // Try save() here so if it fails we haven't contacted the daemon - // This won't be committed unless the HTTP request succeedes anyways - $server->save(); - - if (! empty($newBuild)) { - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'build' => $newBuild, - ], - ]); - } - - DB::commit(); - - return $server; - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Process the variables for a server, and save to the database. - * - * @param \Pterodactyl\Models\Server $server - * @param array $data - * @param bool $admin - * @return \Illuminate\Support\Collection - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - protected function processVariables(Server $server, $data, $admin = false) - { - $server->load('option.variables'); - - if ($admin) { - $server->startup = $data['startup']; - $server->save(); - } - - if ($server->option->variables) { - foreach ($server->option->variables as &$variable) { - $set = isset($data['env_' . $variable->id]); - - // If user is not an admin and are trying to edit a non-editable field - // or an invisible field just silently skip the variable. - if (! $admin && (! $variable->user_editable || ! $variable->user_viewable)) { - continue; - } - - // Perform Field Validation - $validator = Validator::make([ - 'variable_value' => ($set) ? $data['env_' . $variable->id] : null, - ], [ - 'variable_value' => $variable->rules, - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode( - collect([ - 'notice' => ['There was a validation error with the `' . $variable->name . '` variable.'], - ])->merge($validator->errors()->toArray()) - )); - } - - $svar = ServerVariable::firstOrNew([ - 'server_id' => $server->id, - 'variable_id' => $variable->id, - ]); - - // Set the value; if one was not passed set it to the default value - if ($set) { - $svar->variable_value = $data['env_' . $variable->id]; - - // Not passed, check if this record exists if so keep value, otherwise set default - } else { - $svar->variable_value = ($svar->exists) ? $svar->variable_value : $variable->default_value; - } - - $svar->save(); - } - } - - return $this->parseVariables($server); - } - - /** - * Parse the variables and return in a standardized format. - * - * @param \Pterodactyl\Models\Server $server - * @return \Illuminate\Support\Collection - */ - protected function parseVariables(Server $server) - { - // Reload Variables - $server->load('variables'); - - $parsed = $server->option->variables->map(function ($item, $key) use ($server) { - $display = $server->variables->where('variable_id', $item->id)->pluck('variable_value')->first(); - - return [ - 'variable' => $item->env_variable, - 'value' => (! is_null($display)) ? $display : $item->default_value, - ]; - }); - - $merge = [[ - 'variable' => 'STARTUP', - 'value' => $server->startup, - ], [ - 'variable' => 'P_VARIABLE__LOCATION', - 'value' => $server->location->short, - ]]; - - $allocations = $server->allocations->where('id', '!=', $server->allocation_id); - $i = 0; - - foreach ($allocations as $allocation) { - $merge[] = [ - 'variable' => 'ALLOC_' . $i . '__PORT', - 'value' => $allocation->port, - ]; - - $i++; - } - - if ($parsed->count() === 0) { - return collect($merge); - } - - return $parsed->merge($merge); - } - - /** - * Update the startup details for a server. - * - * @param int $id - * @param array $data - * @param bool $admin - * @return bool - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function updateStartup($id, array $data, $admin = false) - { - $server = Server::with('variables', 'option.variables')->findOrFail($id); - $hasServiceChanges = false; - - if ($admin) { - // User is an admin, lots of things to do here. - $validator = Validator::make($data, [ - 'startup' => 'required|string', - 'skip_scripts' => 'sometimes|required|boolean', - 'service_id' => 'required|numeric|min:1|exists:services,id', - 'option_id' => 'required|numeric|min:1|exists:service_options,id', - 'pack_id' => 'sometimes|nullable|numeric|min:0', - ]); - - if ((int) $data['pack_id'] < 1) { - $data['pack_id'] = null; - } - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if ( - $server->service_id != $data['service_id'] || - $server->option_id != $data['option_id'] || - $server->pack_id != $data['pack_id'] - ) { - $hasServiceChanges = true; - } - } - - // If user isn't an administrator, this function is being access from the front-end - // Just try to update specific variables. - if (! $admin || ! $hasServiceChanges) { - return DB::transaction(function () use ($admin, $data, $server) { - $environment = $this->processVariables($server, $data, $admin); - - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'build' => [ - 'env|overwrite' => $environment->pluck('value', 'variable')->toArray(), - ], - ], - ]); - - return false; - }); - } - - // Validate those Service Option Variables - // We know the service and option exists because of the validation. - // We need to verify that the option exists for the service, and then check for - // any required variable fields. (fields are labeled env_) - $option = ServiceOption::where('id', $data['option_id'])->where('service_id', $data['service_id'])->first(); - if (! $option) { - throw new DisplayException('The requested service option does not exist for the specified service.'); - } - - // Validate the Pack - if (! isset($data['pack_id']) || (int) $data['pack_id'] < 1) { - $data['pack_id'] = null; - } else { - $pack = Pack::where('id', $data['pack_id'])->where('option_id', $data['option_id'])->first(); - if (! $pack) { - throw new DisplayException('The requested service pack does not seem to exist for this combination.'); - } - } - - return DB::transaction(function () use ($admin, $data, $server) { - $server->installed = 0; - $server->service_id = $data['service_id']; - $server->option_id = $data['option_id']; - $server->pack_id = $data['pack_id']; - $server->skip_scripts = isset($data['skip_scripts']); - $server->save(); - - $server->variables->each->delete(); - - $server->load('service', 'pack'); - - // Send New Environment - $environment = $this->processVariables($server, $data, $admin); - - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('POST', '/server/reinstall', [ - 'json' => [ - 'build' => [ - 'env|overwrite' => $environment->pluck('value', 'variable')->toArray(), - ], - 'service' => [ - 'type' => $server->option->service->folder, - 'option' => $server->option->tag, - 'pack' => (! is_null($server->pack_id)) ? $server->pack->uuid : null, - 'skip_scripts' => $server->skip_scripts, - ], - ], - ]); - - return true; - }); - } - - /** - * Delete a server from the system permanetly. - * - * @param int $id - * @param bool $force - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id, $force = false) - { - $server = Server::with('node', 'allocations', 'variables')->findOrFail($id); - - // Due to MySQL lockouts if the daemon response fails, we need to - // delete the server from the daemon first. If it succeedes and then - // MySQL fails, users just need to force delete the server. - // - // If this is a force delete, continue anyways. - try { - $server->node->guzzleClient([ - 'X-Access-Token' => $server->node->daemonSecret, - 'X-Access-Server' => $server->uuid, - ])->request('DELETE', '/servers'); - } catch (ClientException $ex) { - // Exception is thrown on 4XX HTTP errors, so catch and determine - // if we should continue, or if there is a permissions error. - // - // Daemon throws a 404 if the server doesn't exist, if that is returned - // continue with deletion, even if not a force deletion. - $response = $ex->getResponse(); - if ($ex->getResponse()->getStatusCode() !== 404 && ! $force) { - throw new DisplayException($ex->getMessage()); - } - } catch (TransferException $ex) { - if (! $force) { - throw new DisplayException($ex->getMessage()); - } - } catch (\Exception $ex) { - throw $ex; - } - - DB::transaction(function () use ($server) { - $server->allocations->each(function ($item) { - $item->server_id = null; - $item->save(); - }); - - $server->variables->each->delete(); - - $server->load('subusers.permissions'); - $server->subusers->each(function ($subuser) { - $subuser->permissions->each->delete(); - $subuser->delete(); - }); - - $server->tasks->each->delete(); - - // Delete Databases - // This is the one un-recoverable point where - // transactions will not save us. - $repository = new DatabaseRepository; - $server->databases->each(function ($item) use ($repository) { - $repository->drop($item->id); - }); - - // Fully delete the server. - $server->delete(); - }); - } - - /** - * Toggle the install status of a serve. - * - * @param int $id - * @return bool - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function toggleInstall($id) - { - $server = Server::findOrFail($id); - if ($server->installed > 1) { - throw new DisplayException('This server was marked as having a failed install or being deleted, you cannot override this.'); - } - $server->installed = ! $server->installed; - - return $server->save(); - } - - /** - * Suspends or unsuspends a server. - * - * @param int $id - * @param bool $unsuspend - * @return void - */ - public function toggleAccess($id, $unsuspend = true) - { - $server = Server::with('node')->findOrFail($id); - - DB::transaction(function () use ($server, $unsuspend) { - if ( - (! $unsuspend && $server->suspended) || - ($unsuspend && ! $server->suspended) - ) { - return true; - } - - $server->suspended = ! $unsuspend; - $server->save(); - - $server->node->guzzleClient([ - 'X-Access-Token' => $server->node->daemonSecret, - 'X-Access-Server' => $server->uuid, - ])->request('POST', ($unsuspend) ? '/server/unsuspend' : '/server/suspend'); - }); - } - - /** - * Updates the SFTP password for a server. - * - * @param int $id - * @param string $password - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function updateSFTPPassword($id, $password) - { - $server = Server::with('node')->findOrFail($id); - - $validator = Validator::make(['password' => $password], [ - 'password' => 'required|regex:/^((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})$/', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::transaction(function () use ($password, $server) { - $server->sftp_password = Crypt::encrypt($password); - $server->save(); - - $server->node->guzzleClient([ - 'X-Access-Token' => $server->node->daemonSecret, - 'X-Access-Server' => $server->uuid, - ])->request('POST', '/server/password', [ - 'json' => ['password' => $password], - ]); - }); - } - - /** - * Marks a server for reinstallation on the node. - * - * @param int $id - * @return void - */ - public function reinstall($id) - { - $server = Server::with('node')->findOrFail($id); - - DB::transaction(function () use ($server) { - $server->installed = 0; - $server->save(); - - $server->node->guzzleClient([ - 'X-Access-Token' => $server->node->daemonSecret, - 'X-Access-Server' => $server->uuid, - ])->request('POST', '/server/reinstall'); - }); - } -} diff --git a/app/Repositories/Old/old_UserRepository.php b/app/Repositories/Old/old_UserRepository.php deleted file mode 100644 index 6f028a201..000000000 --- a/app/Repositories/Old/old_UserRepository.php +++ /dev/null @@ -1,182 +0,0 @@ - - * Some Modifications (c) 2015 Dylan Seidt . - * - * 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; - -use DB; -use Auth; -use Hash; -use Settings; -use Validator; -use Pterodactyl\Models; -use Pterodactyl\Services\UuidService; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class old_UserRepository -{ - /** - * Creates a user on the panel. Returns the created user's ID. - * - * @param array $data - * @return \Pterodactyl\Models\User - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'email' => 'required|email|unique:users,email', - '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' => 'sometimes|nullable|unique:users,id', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - - try { - $user = new Models\User; - $uuid = new UuidService; - - // Support for API Services - 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->fill([ - 'email' => $data['email'], - 'username' => $data['username'], - 'name_first' => $data['name_first'], - 'name_last' => $data['name_last'], - 'password' => (empty($data['password'])) ? 'unset' : Hash::make($data['password']), - 'root_admin' => $data['root_admin'], - 'language' => Settings::get('default_language', 'en'), - ]); - $user->save(); - - DB::commit(); - - return $user; - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Updates a user on the panel. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\User - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $user = Models\User::findOrFail($id); - - $validator = Validator::make($data, [ - 'email' => 'sometimes|required|email|unique:users,email,' . $id, - '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', - 'totp_secret' => 'sometimes|required|size:16', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - // The password and root_admin fields are not mass assignable. - if (! empty($data['password'])) { - $data['password'] = Hash::make($data['password']); - } else { - unset($data['password']); - } - - $user->fill($data)->save(); - - return $user; - } - - /** - * Deletes a user on the panel. - * - * @param int $id - * @return void - * @todo Move user self-deletion checking to the controller, rather than the repository. - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $user = Models\User::findOrFail($id); - - if (Models\Server::where('owner_id', $id)->count() > 0) { - throw new DisplayException('Cannot delete a user with active servers attached to thier account.'); - } - - if (! is_null(Auth::user()) && (int) Auth::user()->id === (int) $id) { - throw new DisplayException('Cannot delete your own account.'); - } - - DB::beginTransaction(); - - try { - foreach (Models\Subuser::with('permissions')->where('user_id', $id)->get() as &$subuser) { - foreach ($subuser->permissions as &$permission) { - $permission->delete(); - } - - $subuser->delete(); - } - - $user->delete(); - DB::commit(); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } -} diff --git a/app/Services/UserService.php b/app/Services/Users/CreationService.php similarity index 72% rename from app/Services/UserService.php rename to app/Services/Users/CreationService.php index a7c87c573..f6a60f1c0 100644 --- a/app/Services/UserService.php +++ b/app/Services/Users/CreationService.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Services; +namespace Pterodactyl\Services\Users; use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; @@ -32,7 +32,7 @@ use Pterodactyl\Notifications\AccountCreated; use Pterodactyl\Services\Helpers\TemporaryPasswordService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -class UserService +class CreationService { /** * @var \Illuminate\Foundation\Application @@ -40,9 +40,9 @@ class UserService protected $app; /** - * @var \Illuminate\Database\Connection + * @var \Illuminate\Database\ConnectionInterface */ - protected $database; + protected $connection; /** * @var \Illuminate\Contracts\Hashing\Hasher @@ -65,25 +65,25 @@ class UserService protected $repository; /** - * UserService constructor. + * CreationService constructor. * - * @param \Illuminate\Foundation\Application $application - * @param \Illuminate\Notifications\ChannelManager $notification - * @param \Illuminate\Database\ConnectionInterface $database - * @param \Illuminate\Contracts\Hashing\Hasher $hasher - * @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService - * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + * @param \Illuminate\Foundation\Application $application + * @param \Illuminate\Notifications\ChannelManager $notification + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Illuminate\Contracts\Hashing\Hasher $hasher + * @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( Application $application, ChannelManager $notification, - ConnectionInterface $database, + ConnectionInterface $connection, Hasher $hasher, TemporaryPasswordService $passwordService, UserRepositoryInterface $repository ) { $this->app = $application; - $this->database = $database; + $this->connection = $connection; $this->hasher = $hasher; $this->notification = $notification; $this->passwordService = $passwordService; @@ -99,25 +99,22 @@ class UserService * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function create(array $data) + public function handle(array $data) { if (array_key_exists('password', $data) && ! empty($data['password'])) { $data['password'] = $this->hasher->make($data['password']); } - // Begin Transaction - $this->database->beginTransaction(); - + $this->connection->beginTransaction(); if (! isset($data['password']) || empty($data['password'])) { $data['password'] = $this->hasher->make(str_random(30)); $token = $this->passwordService->generateReset($data['email']); } $user = $this->repository->create($data); + $this->connection->commit(); - // Persist the data - $this->database->commit(); - + // @todo fire event, handle notification there $this->notification->send($user, $this->app->makeWith(AccountCreated::class, [ 'user' => [ 'name' => $user->name_first, @@ -128,24 +125,4 @@ class UserService return $user; } - - /** - * Update the user model instance. - * - * @param int $id - * @param array $data - * @return mixed - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - */ - public function update($id, array $data) - { - if (isset($data['password'])) { - $data['password'] = $this->hasher->make($data['password']); - } - - $user = $this->repository->update($id, $data); - - return $user; - } } diff --git a/app/Services/Users/DeletionService.php b/app/Services/Users/DeletionService.php new file mode 100644 index 000000000..5bf6a5b01 --- /dev/null +++ b/app/Services/Users/DeletionService.php @@ -0,0 +1,88 @@ +. + * + * 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\Services\Users; + +use Illuminate\Contracts\Translation\Translator; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\User; + +class DeletionService +{ + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * DeletionService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Illuminate\Contracts\Translation\Translator $translator + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + */ + public function __construct( + ServerRepositoryInterface $serverRepository, + Translator $translator, + UserRepositoryInterface $repository + ) { + $this->repository = $repository; + $this->translator = $translator; + $this->serverRepository = $serverRepository; + } + + /** + * Delete a user from the panel only if they have no servers attached to their account. + * + * @param int|\Pterodactyl\Models\User $user + * @return bool|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function handle($user) + { + if (! $user instanceof User) { + $user = $this->repository->find($user); + } + + $servers = $this->serverRepository->findWhere([['owner_id', '=', $user->id]]); + if (count($servers) > 0) { + throw new DisplayException($this->translator->trans('admin/user.exceptions.user_has_servers')); + } + + return $this->repository->delete($user->id); + } +} diff --git a/app/Services/Users/UpdateService.php b/app/Services/Users/UpdateService.php new file mode 100644 index 000000000..6df7dc583 --- /dev/null +++ b/app/Services/Users/UpdateService.php @@ -0,0 +1,75 @@ +. + * + * 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\Services\Users; + +use Illuminate\Contracts\Hashing\Hasher; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; + +class UpdateService +{ + /** + * @var \Illuminate\Contracts\Hashing\Hasher + */ + protected $hasher; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * UpdateService constructor. + * + * @param \Illuminate\Contracts\Hashing\Hasher $hasher + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + */ + public function __construct( + Hasher $hasher, + UserRepositoryInterface $repository + ) { + $this->hasher = $hasher; + $this->repository = $repository; + } + + /** + * Update the user model instance. + * + * @param int $id + * @param array $data + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle($id, array $data) + { + if (isset($data['password'])) { + $data['password'] = $this->hasher->make($data['password']); + } + + $user = $this->repository->update($id, $data); + + return $user; + } +} diff --git a/resources/lang/en/admin/user.php b/resources/lang/en/admin/user.php new file mode 100644 index 000000000..b8d38d323 --- /dev/null +++ b/resources/lang/en/admin/user.php @@ -0,0 +1,33 @@ +. + * + * 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. + */ + +return [ + 'exceptions' => [ + 'user_has_servers' => 'Cannot delete a user with active servers attached to their account. Please delete their server\'s before continuing.', + ], + 'notices' => [ + 'account_created' => 'Account has been created successfully.', + 'account_updated' => 'Account has been successfully updated.', + ], +]; diff --git a/tests/Unit/Services/UserServiceTest.php b/tests/Unit/Services/Users/CreationServiceTest.php similarity index 80% rename from tests/Unit/Services/UserServiceTest.php rename to tests/Unit/Services/Users/CreationServiceTest.php index f02c56525..59067f9d4 100644 --- a/tests/Unit/Services/UserServiceTest.php +++ b/tests/Unit/Services/Users/CreationServiceTest.php @@ -25,17 +25,17 @@ namespace Tests\Unit\Services; use Mockery as m; +use Pterodactyl\Services\Users\CreationService; use Tests\TestCase; use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Database\ConnectionInterface; use Illuminate\Notifications\ChannelManager; use Pterodactyl\Notifications\AccountCreated; -use Pterodactyl\Services\UserService; use Pterodactyl\Services\Helpers\TemporaryPasswordService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -class UserServiceTest extends TestCase +class CreationServiceTest extends TestCase { /** * @var \Illuminate\Foundation\Application @@ -68,7 +68,7 @@ class UserServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\UserService + * @var \Pterodactyl\Services\Users\CreationService */ protected $service; @@ -86,7 +86,7 @@ class UserServiceTest extends TestCase $this->passwordService = m::mock(TemporaryPasswordService::class); $this->repository = m::mock(UserRepositoryInterface::class); - $this->service = new UserService( + $this->service = new CreationService( $this->appMock, $this->notification, $this->database, @@ -99,7 +99,7 @@ class UserServiceTest extends TestCase /** * Test that a user is created when a password is passed. */ - public function test_user_creation_with_password() + public function testUserIsCreatedWhenPasswordIsProvided() { $user = (object) [ 'name_first' => 'FirstName', @@ -122,7 +122,7 @@ class UserServiceTest extends TestCase $this->notification->shouldReceive('send')->with($user, null)->once()->andReturnNull(); - $response = $this->service->create([ + $response = $this->service->handle([ 'password' => 'raw-password', ]); @@ -134,7 +134,7 @@ class UserServiceTest extends TestCase /** * Test that a user is created with a random password when no password is provided. */ - public function test_user_creation_without_password() + public function testUserIsCreatedWhenNoPasswordIsProvided() { $user = (object) [ 'name_first' => 'FirstName', @@ -145,7 +145,10 @@ class UserServiceTest extends TestCase $this->hasher->shouldNotReceive('make'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->hasher->shouldReceive('make')->once()->andReturn('created-enc-password'); - $this->passwordService->shouldReceive('generateReset')->with('user@example.com')->once()->andReturn('random-token'); + $this->passwordService->shouldReceive('generateReset') + ->with('user@example.com') + ->once() + ->andReturn('random-token'); $this->repository->shouldReceive('create')->with([ 'password' => 'created-enc-password', @@ -163,7 +166,7 @@ class UserServiceTest extends TestCase $this->notification->shouldReceive('send')->with($user, null)->once()->andReturnNull(); - $response = $this->service->create([ + $response = $this->service->handle([ 'email' => 'user@example.com', ]); @@ -172,31 +175,4 @@ class UserServiceTest extends TestCase $this->assertEquals($user->name_first, 'FirstName'); $this->assertEquals($user->email, $response->email); } - - /** - * Test that passing no password will not attempt any hashing. - */ - public function test_user_update_without_password() - { - $this->hasher->shouldNotReceive('make'); - $this->repository->shouldReceive('update')->with(1, ['email' => 'new@example.com'])->once()->andReturnNull(); - - $response = $this->service->update(1, ['email' => 'new@example.com']); - - $this->assertNull($response); - } - - /** - * Test that passing a password will hash it before storage. - */ - public function test_user_update_with_password() - { - $this->hasher->shouldReceive('make')->with('password')->once()->andReturn('enc-password'); - $this->repository->shouldReceive('update')->with(1, ['password' => 'enc-password'])->once()->andReturnNull(); - - $response = $this->service->update(1, ['password' => 'password']); - - $this->assertNull($response); - } - } diff --git a/tests/Unit/Services/Users/DeletionServiceTest.php b/tests/Unit/Services/Users/DeletionServiceTest.php new file mode 100644 index 000000000..6f21096e4 --- /dev/null +++ b/tests/Unit/Services/Users/DeletionServiceTest.php @@ -0,0 +1,120 @@ +. + * + * 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 Tests\Unit\Services\Users; + +use Illuminate\Contracts\Translation\Translator; +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Models\User; +use Pterodactyl\Services\Users\DeletionService; +use Tests\TestCase; + +class DeletionServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Services\Users\DeletionService + */ + protected $service; + + /** + * @var User + */ + protected $user; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->user = factory(User::class)->make(); + $this->repository = m::mock(UserRepositoryInterface::class); + $this->translator = m::mock(Translator::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + + $this->service = new DeletionService( + $this->serverRepository, $this->translator, $this->repository + ); + } + + /** + * Test that a user is deleted if they have no servers. + */ + public function testUserIsDeletedIfNoServersAreAttachedToAccount() + { + $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn([]); + $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); + + $this->assertTrue( + $this->service->handle($this->user), + 'Assert that service responds true.' + ); + } + + /** + * Test that an exception is thrown if trying to delete a user with servers. + * + * @expectedException \Pterodactyl\Exceptions\DisplayException + */ + public function testExceptionIsThrownIfServersAreAttachedToAccount() + { + $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(['item']); + $this->translator->shouldReceive('trans')->with('admin/user.exceptions.user_has_servers')->once()->andReturnNull(); + + $this->service->handle($this->user); + } + + /** + * Test that the function supports passing in a model or an ID. + */ + public function testIntegerCanBePassedInPlaceOfUserModel() + { + $this->repository->shouldReceive('find')->with($this->user->id)->once()->andReturn($this->user); + $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn([]); + $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); + + $this->assertTrue( + $this->service->handle($this->user->id), + 'Assert that service responds true.' + ); + } +} diff --git a/tests/Unit/Services/Users/UpdateServiceTest.php b/tests/Unit/Services/Users/UpdateServiceTest.php new file mode 100644 index 000000000..399a2f856 --- /dev/null +++ b/tests/Unit/Services/Users/UpdateServiceTest.php @@ -0,0 +1,83 @@ +. + * + * 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 Tests\Unit\Services\Users; + +use Illuminate\Contracts\Hashing\Hasher; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Services\Users\UpdateService; +use Tests\TestCase; +use Mockery as m; + +class UpdateServiceTest extends TestCase +{ + /** + * @var \Illuminate\Contracts\Hashing\Hasher + */ + protected $hasher; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Users\UpdateService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->hasher = m::mock(Hasher::class); + $this->repository = m::mock(UserRepositoryInterface::class); + + $this->service = new UpdateService($this->hasher, $this->repository); + } + + /** + * Test that the handle function does not attempt to hash a password if no password is passed. + */ + public function testUpdateUserWithoutTouchingHasherIfNoPasswordPassed() + { + $this->repository->shouldReceive('update')->with(1, ['test-data' => 'value'])->once()->andReturnNull(); + + $this->assertNull($this->service->handle(1, ['test-data' => 'value'])); + } + + /** + * Test that the handle function hashes a password if passed in the data array. + */ + public function testUpdateUserAndHashPasswordIfProvided() + { + $this->hasher->shouldReceive('make')->with('raw_pass')->once()->andReturn('enc_pass'); + $this->repository->shouldReceive('update')->with(1, ['password' => 'enc_pass'])->once()->andReturnNull(); + + $this->assertNull($this->service->handle(1, ['password' => 'raw_pass'])); + } +}