diff --git a/app/Http/Controllers/Api/Client/Servers/NetworkController.php b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php similarity index 54% rename from app/Http/Controllers/Api/Client/Servers/NetworkController.php rename to app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php index e1421750..162d815d 100644 --- a/app/Http/Controllers/Api/Client/Servers/NetworkController.php +++ b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php @@ -3,16 +3,19 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; use Pterodactyl\Models\Server; +use Illuminate\Http\JsonResponse; +use Pterodactyl\Models\Allocation; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Repositories\Eloquent\ServerRepository; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Transformers\Api\Client\AllocationTransformer; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest; +use Pterodactyl\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest; +use Pterodactyl\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest; -class NetworkController extends ClientApiController +class NetworkAllocationController extends ClientApiController { /** * @var \Pterodactyl\Repositories\Eloquent\AllocationRepository @@ -58,33 +61,67 @@ class NetworkController extends ClientApiController /** * Set the primary allocation for a server. * - * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest $request + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest $request * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Allocation $allocation * @return array * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function storePrimary(SetPrimaryAllocationRequest $request, Server $server): array + public function update(UpdateAllocationRequest $request, Server $server, Allocation $allocation): array { - try { - /** @var \Pterodactyl\Models\Allocation $allocation */ - $allocation = $this->repository->findFirstWhere([ - 'server_id' => $server->id, - 'ip' => $request->input('ip'), - 'port' => $request->input('port'), - ]); - } catch (ModelNotFoundException $exception) { - throw new DisplayException( - 'The IP and port you selected are not available for this server.' - ); - } + $allocation = $this->repository->update($allocation->id, [ + 'notes' => $request->input('notes'), + ]); + return $this->fractal->item($allocation) + ->transformWith($this->getTransformer(AllocationTransformer::class)) + ->toArray(); + } + + /** + * Set the primary allocation for a server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Allocation $allocation + * @return array + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function setPrimary(SetPrimaryAllocationRequest $request, Server $server, Allocation $allocation): array + { $this->serverRepository->update($server->id, ['allocation_id' => $allocation->id]); return $this->fractal->item($allocation) ->transformWith($this->getTransformer(AllocationTransformer::class)) ->toArray(); } + + /** + * Delete an allocation from a server. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Allocation $allocation + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation) + { + if ($allocation->id === $server->allocation_id) { + throw new DisplayException( + 'Cannot delete the primary allocation for a server.' + ); + } + + $this->repository->update($allocation->id, ['server_id' => null, 'notes' => null]); + + return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + } } diff --git a/app/Http/Middleware/Api/Client/Server/AllocationBelongsToServer.php b/app/Http/Middleware/Api/Client/Server/AllocationBelongsToServer.php new file mode 100644 index 00000000..d027d563 --- /dev/null +++ b/app/Http/Middleware/Api/Client/Server/AllocationBelongsToServer.php @@ -0,0 +1,33 @@ +route()->parameter('server'); + /** @var \Pterodactyl\Models\Allocation|null $allocation */ + $allocation = $request->route()->parameter('allocation'); + + if ($allocation && $allocation->server_id !== $server->id) { + throw new NotFoundHttpException; + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php index 81ed6b40..0bd40eee 100644 --- a/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php +++ b/app/Http/Middleware/Api/Client/SubstituteClientApiBindings.php @@ -4,12 +4,12 @@ namespace Pterodactyl\Http\Middleware\Api\Client; use Closure; use Pterodactyl\Models\Backup; +use Pterodactyl\Models\Database; use Illuminate\Container\Container; use Pterodactyl\Contracts\Extensions\HashidsInterface; use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; class SubstituteClientApiBindings extends ApiSubstituteBindings { @@ -43,17 +43,9 @@ class SubstituteClientApiBindings extends ApiSubstituteBindings }); $this->router->bind('database', function ($value) use ($request) { - try { - $id = Container::getInstance()->make(HashidsInterface::class)->decodeFirst($value); + $id = Container::getInstance()->make(HashidsInterface::class)->decodeFirst($value); - return Container::getInstance()->make(DatabaseRepositoryInterface::class)->findFirstWhere([ - ['id', '=', $id], - ]); - } catch (RecordNotFoundException $exception) { - $request->attributes->set('is_missing_model', true); - - return null; - } + return Database::query()->where('id', $id)->firstOrFail(); }); $this->router->model('backup', Backup::class, function ($value) { diff --git a/app/Http/Requests/Api/Client/Servers/Network/DeleteAllocationRequest.php b/app/Http/Requests/Api/Client/Servers/Network/DeleteAllocationRequest.php new file mode 100644 index 00000000..9c0d911f --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Network/DeleteAllocationRequest.php @@ -0,0 +1,17 @@ + 'required|string', - 'port' => 'required|numeric|min:1024|max:65535', - ]; + return []; } } diff --git a/app/Http/Requests/Api/Client/Servers/Network/UpdateAllocationRequest.php b/app/Http/Requests/Api/Client/Servers/Network/UpdateAllocationRequest.php new file mode 100644 index 00000000..1424cad1 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Network/UpdateAllocationRequest.php @@ -0,0 +1,28 @@ + Allocation::$validationRules['notes'], + ]; + } +} diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index 5f243562..81e59652 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -9,6 +9,7 @@ namespace Pterodactyl\Models; * @property string|null $ip_alias * @property int $port * @property int|null $server_id + * @property string|null $notes * @property \Carbon\Carbon|null $created_at * @property \Carbon\Carbon|null $updated_at * @@ -60,6 +61,7 @@ class Allocation extends Model 'port' => 'required|numeric|between:1024,65553', 'ip_alias' => 'nullable|string', 'server_id' => 'nullable|exists:servers,id', + 'notes' => 'nullable|string|max:256', ]; /** diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 772ae5e4..af3dc5cf 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -44,7 +44,9 @@ class Permission extends Model const ACTION_BACKUP_DOWNLOAD = 'backup.download'; const ACTION_ALLOCATION_READ = 'allocation.read'; - const ACTION_ALLOCIATION_UPDATE = 'allocation.update'; + const ACTION_ALLOCATION_CREATE = 'allocation.create'; + const ACTION_ALLOCATION_UPDATE = 'allocation.update'; + const ACTION_ALLOCATION_DELETE = 'allocation.delete'; const ACTION_FILE_READ = 'file.read'; const ACTION_FILE_CREATE = 'file.create'; @@ -157,7 +159,9 @@ class Permission extends Model 'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.', 'keys' => [ 'read' => 'Allows a user to view the allocations assigned to this server.', - 'update' => 'Allows a user to modify the allocations assigned to this server.', + 'create' => 'Allows a user to assign additional allocations to the server.', + 'update' => 'Allows a user to change the primary server allocation and attach notes to each allocation.', + 'delete' => 'Allows a user to delete an allocation from the server.', ], ], diff --git a/app/Transformers/Api/Application/AllocationTransformer.php b/app/Transformers/Api/Application/AllocationTransformer.php index e4fa1bb5..d0c71e63 100644 --- a/app/Transformers/Api/Application/AllocationTransformer.php +++ b/app/Transformers/Api/Application/AllocationTransformer.php @@ -39,6 +39,7 @@ class AllocationTransformer extends BaseTransformer 'ip' => $allocation->ip, 'alias' => $allocation->ip_alias, 'port' => $allocation->port, + 'notes' => $allocation->notes, 'assigned' => ! is_null($allocation->server_id), ]; } diff --git a/app/Transformers/Api/Client/AllocationTransformer.php b/app/Transformers/Api/Client/AllocationTransformer.php index 8f83aac7..57f72eaf 100644 --- a/app/Transformers/Api/Client/AllocationTransformer.php +++ b/app/Transformers/Api/Client/AllocationTransformer.php @@ -25,9 +25,11 @@ class AllocationTransformer extends BaseClientTransformer public function transform(Allocation $model) { return [ + 'id' => $model->id, 'ip' => $model->ip, 'ip_alias' => $model->ip_alias, 'port' => $model->port, + 'notes' => $model->notes, 'is_default' => $model->server->allocation_id === $model->id, ]; } diff --git a/database/migrations/2020_07_09_201845_add_notes_column_for_allocations.php b/database/migrations/2020_07_09_201845_add_notes_column_for_allocations.php new file mode 100644 index 00000000..ed71f9c2 --- /dev/null +++ b/database/migrations/2020_07_09_201845_add_notes_column_for_allocations.php @@ -0,0 +1,32 @@ +string('notes')->nullable()->after('server_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('allocations', function (Blueprint $table) { + $table->dropColumn('notes'); + }); + } +} diff --git a/resources/scripts/api/server/getServer.ts b/resources/scripts/api/server/getServer.ts index 34765337..7072033f 100644 --- a/resources/scripts/api/server/getServer.ts +++ b/resources/scripts/api/server/getServer.ts @@ -2,9 +2,11 @@ import http, { FractalResponseData, FractalResponseList } from '@/api/http'; import { rawDataToServerAllocation } from '@/api/transformers'; export interface Allocation { + id: number; ip: string; alias: string | null; port: number; + notes: string | null; isDefault: boolean; } diff --git a/resources/scripts/api/server/network/deleteServerAllocation.ts b/resources/scripts/api/server/network/deleteServerAllocation.ts new file mode 100644 index 00000000..92fd4b30 --- /dev/null +++ b/resources/scripts/api/server/network/deleteServerAllocation.ts @@ -0,0 +1,4 @@ +import { Allocation } from '@/api/server/getServer'; +import http from '@/api/http'; + +export default async (uuid: string, id: number): Promise => await http.delete(`/api/client/servers/${uuid}/network/allocations/${id}`); diff --git a/resources/scripts/api/server/network/getServerAllocations.ts b/resources/scripts/api/server/network/getServerAllocations.ts index 47ffcec9..7309bd26 100644 --- a/resources/scripts/api/server/network/getServerAllocations.ts +++ b/resources/scripts/api/server/network/getServerAllocations.ts @@ -3,7 +3,7 @@ import { rawDataToServerAllocation } from '@/api/transformers'; import { Allocation } from '@/api/server/getServer'; export default async (uuid: string): Promise => { - const { data } = await http.get(`/api/client/servers/${uuid}/network`); + const { data } = await http.get(`/api/client/servers/${uuid}/network/allocations`); return (data.data || []).map(rawDataToServerAllocation); }; diff --git a/resources/scripts/api/server/network/setPrimaryServerAllocation.ts b/resources/scripts/api/server/network/setPrimaryServerAllocation.ts index f63f2d52..27c09b72 100644 --- a/resources/scripts/api/server/network/setPrimaryServerAllocation.ts +++ b/resources/scripts/api/server/network/setPrimaryServerAllocation.ts @@ -2,8 +2,8 @@ import { Allocation } from '@/api/server/getServer'; import http from '@/api/http'; import { rawDataToServerAllocation } from '@/api/transformers'; -export default async (uuid: string, ip: string, port: number): Promise => { - const { data } = await http.put(`/api/client/servers/${uuid}/network/primary`, { ip, port }); +export default async (uuid: string, id: number): Promise => { + const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations/${id}/primary`); return rawDataToServerAllocation(data); }; diff --git a/resources/scripts/api/server/network/setServerAllocationNotes.ts b/resources/scripts/api/server/network/setServerAllocationNotes.ts new file mode 100644 index 00000000..4531dc75 --- /dev/null +++ b/resources/scripts/api/server/network/setServerAllocationNotes.ts @@ -0,0 +1,9 @@ +import { Allocation } from '@/api/server/getServer'; +import http from '@/api/http'; +import { rawDataToServerAllocation } from '@/api/transformers'; + +export default async (uuid: string, id: number, notes: string | null): Promise => { + const { data } = await http.post(`/api/client/servers/${uuid}/network/allocations/${id}`, { notes }); + + return rawDataToServerAllocation(data); +}; diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index 2aad632b..eb54b62f 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -2,8 +2,10 @@ import { Allocation } from '@/api/server/getServer'; import { FractalResponseData } from '@/api/http'; export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({ + id: data.attributes.id, ip: data.attributes.ip, alias: data.attributes.ip_alias, port: data.attributes.port, + notes: data.attributes.notes, isDefault: data.attributes.is_default, }); diff --git a/resources/scripts/components/server/network/NetworkContainer.tsx b/resources/scripts/components/server/network/NetworkContainer.tsx index 4d83e218..dcf92ef8 100644 --- a/resources/scripts/components/server/network/NetworkContainer.tsx +++ b/resources/scripts/components/server/network/NetworkContainer.tsx @@ -23,16 +23,17 @@ const NetworkContainer = () => { const { clearFlashes, clearAndAddHttpError } = useFlash(); const { data, error, mutate } = useSWR(server.uuid, key => getServerAllocations(key), { initialData: server.allocations }); - const setPrimaryAllocation = (ip: string, port: number) => { + const setPrimaryAllocation = (id: number) => { clearFlashes('server:network'); - mutate(data?.map(a => (a.ip === ip && a.port === port) ? { ...a, isDefault: true } : { - ...a, - isDefault: false, - }), false); + const initial = data; + mutate(data?.map(a => a.id === id ? { ...a, isDefault: true } : { ...a, isDefault: false }), false); - setPrimaryServerAllocation(server.uuid, ip, port) - .catch(error => clearAndAddHttpError({ key: 'server:network', error })); + setPrimaryServerAllocation(server.uuid, id) + .catch(error => { + clearAndAddHttpError({ key: 'server:network', error }); + mutate(initial, false); + }); }; useEffect(() => { @@ -46,7 +47,7 @@ const NetworkContainer = () => { {!data ? : - data.map(({ ip, port, alias, isDefault }, index) => ( + data.map(({ id, ip, port, alias, isDefault }, index) => ( 0 ? tw`mt-2` : undefined}>
@@ -70,7 +71,7 @@ const NetworkContainer = () => { isSecondary size={'xsmall'} color={'primary'} - onClick={() => setPrimaryAllocation(ip, port)} + onClick={() => setPrimaryAllocation(id)} > Make Primary diff --git a/routes/api-client.php b/routes/api-client.php index 9bea1195..31d6d104 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -2,6 +2,7 @@ use Illuminate\Support\Facades\Route; use Pterodactyl\Http\Middleware\Api\Client\Server\AuthenticateServerAccess; +use Pterodactyl\Http\Middleware\Api\Client\Server\AllocationBelongsToServer; /* |-------------------------------------------------------------------------- @@ -74,9 +75,11 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::delete('/{schedule}/tasks/{task}', 'Servers\ScheduleTaskController@delete'); }); - Route::group(['prefix' => '/network'], function () { - Route::get('/', 'Servers\NetworkController@index'); - Route::put('/primary', 'Servers\NetworkController@storePrimary'); + Route::group(['prefix' => '/network', 'middleware' => [AllocationBelongsToServer::class]], function () { + Route::get('/allocations', 'Servers\NetworkAllocationController@index'); + Route::post('/allocations/{allocation}', 'Servers\NetworkAllocationController@update'); + Route::post('/allocations/{allocation}/primary', 'Servers\NetworkAllocationController@setPrimary'); + Route::delete('/allocations/{allocation}', 'Servers\NetworkAllocationController@delete'); }); Route::group(['prefix' => '/users'], function () {