mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-20 00:34:44 +02:00
Add api for mounts (#160)
* add application api endpoints for mounts * run pint * add mounts resource to api key * add includes to mount transformer * forgot delete route for mount itself * add migration for "r_mounts" column * add mounts to testcase api key
This commit is contained in:
parent
0a5810358a
commit
b1f99ca8a3
165
app/Http/Controllers/Api/Application/Mounts/MountController.php
Normal file
165
app/Http/Controllers/Api/Application/Mounts/MountController.php
Normal file
@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Application\Mounts;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Contracts\Translation\Translator;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use App\Models\Mount;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Transformers\Api\Application\MountTransformer;
|
||||
use App\Http\Requests\Api\Application\Mounts\GetMountRequest;
|
||||
use App\Http\Requests\Api\Application\Mounts\StoreMountRequest;
|
||||
use App\Http\Requests\Api\Application\Mounts\DeleteMountRequest;
|
||||
use App\Http\Requests\Api\Application\Mounts\UpdateMountRequest;
|
||||
use App\Exceptions\Service\HasActiveServersException;
|
||||
|
||||
class MountController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* MountController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Translator $translator
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the mounts currently available on the Panel.
|
||||
*/
|
||||
public function index(GetMountRequest $request): array
|
||||
{
|
||||
$mounts = QueryBuilder::for(Mount::query())
|
||||
->allowedFilters(['uuid', 'name'])
|
||||
->allowedSorts(['id', 'uuid'])
|
||||
->paginate($request->query('per_page') ?? 50);
|
||||
|
||||
return $this->fractal->collection($mounts)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data for a single instance of a mount.
|
||||
*/
|
||||
public function view(GetMountRequest $request, Mount $mount): array
|
||||
{
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new mount on the Panel. Returns the created mount and an HTTP/201
|
||||
* status response on success.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function store(StoreMountRequest $request): JsonResponse
|
||||
{
|
||||
$model = (new Mount())->fill($request->validated());
|
||||
$model->forceFill(['uuid' => Uuid::uuid4()->toString()]);
|
||||
|
||||
$model->saveOrFail();
|
||||
$mount = $model->fresh();
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->addMeta([
|
||||
'resource' => route('api.application.mounts.view', [
|
||||
'mount' => $mount->id,
|
||||
]),
|
||||
])
|
||||
->respond(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing mount on the Panel.
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function update(UpdateMountRequest $request, Mount $mount): array
|
||||
{
|
||||
$mount->forceFill($request->validated())->save();
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a given mount from the Panel as long as there are no servers
|
||||
* currently attached to it.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\HasActiveServersException
|
||||
*/
|
||||
public function delete(DeleteMountRequest $request, Mount $mount): JsonResponse
|
||||
{
|
||||
if ($mount->servers()->count() > 0) {
|
||||
throw new HasActiveServersException($this->translator->get('exceptions.mount.servers_attached'));
|
||||
}
|
||||
|
||||
$mount->delete();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds eggs to the mount's many-to-many relation.
|
||||
*/
|
||||
public function addEggs(Request $request, Mount $mount): array
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'eggs' => 'required|exists:eggs,id',
|
||||
]);
|
||||
|
||||
$eggs = $validatedData['eggs'] ?? [];
|
||||
if (count($eggs) > 0) {
|
||||
$mount->eggs()->attach($eggs);
|
||||
}
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds nodes to the mount's many-to-many relation.
|
||||
*/
|
||||
public function addNodes(Request $request, Mount $mount): array
|
||||
{
|
||||
$data = $request->validate(['nodes' => 'required|exists:nodes,id']);
|
||||
|
||||
$nodes = $data['nodes'] ?? [];
|
||||
if (count($nodes) > 0) {
|
||||
$mount->nodes()->attach($nodes);
|
||||
}
|
||||
|
||||
return $this->fractal->item($mount)
|
||||
->transformWith($this->getTransformer(MountTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an egg from the mount's many-to-many relation.
|
||||
*/
|
||||
public function deleteEgg(Mount $mount, int $egg_id): JsonResponse
|
||||
{
|
||||
$mount->eggs()->detach($egg_id);
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a node from the mount's many-to-many relation.
|
||||
*/
|
||||
public function deleteNode(Mount $mount, int $node_id): JsonResponse
|
||||
{
|
||||
$mount->nodes()->detach($node_id);
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Mounts;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class DeleteMountRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
}
|
13
app/Http/Requests/Api/Application/Mounts/GetMountRequest.php
Normal file
13
app/Http/Requests/Api/Application/Mounts/GetMountRequest.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Mounts;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class GetMountRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
|
||||
|
||||
protected int $permission = AdminAcl::READ;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Mounts;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class StoreMountRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_MOUNTS;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Mounts;
|
||||
|
||||
use App\Models\Mount;
|
||||
|
||||
class UpdateMountRequest extends StoreMountRequest
|
||||
{
|
||||
/**
|
||||
* Apply validation rules to this request. Uses the parent class rules()
|
||||
* function but passes in the rules for updating rather than creating.
|
||||
*/
|
||||
public function rules(array $rules = null): array
|
||||
{
|
||||
/** @var Mount $mount */
|
||||
$mount = $this->route()->parameter('mount');
|
||||
|
||||
return parent::rules(Mount::getRulesForUpdate($mount->id));
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property int $r_eggs
|
||||
* @property int $r_database_hosts
|
||||
* @property int $r_server_databases
|
||||
* @property int $r_mounts
|
||||
* @property \App\Models\User $tokenable
|
||||
* @property \App\Models\User $user
|
||||
*
|
||||
@ -83,7 +84,7 @@ class ApiKey extends Model
|
||||
*/
|
||||
public const KEY_LENGTH = 32;
|
||||
|
||||
public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases'];
|
||||
public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases', 'mounts'];
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
@ -109,6 +110,7 @@ class ApiKey extends Model
|
||||
'r_' . AdminAcl::RESOURCE_EGGS,
|
||||
'r_' . AdminAcl::RESOURCE_NODES,
|
||||
'r_' . AdminAcl::RESOURCE_SERVERS,
|
||||
'r_' . AdminAcl::RESOURCE_MOUNTS,
|
||||
];
|
||||
|
||||
/**
|
||||
@ -137,6 +139,7 @@ class ApiKey extends Model
|
||||
'r_' . AdminAcl::RESOURCE_EGGS => 'integer|min:0|max:3',
|
||||
'r_' . AdminAcl::RESOURCE_NODES => 'integer|min:0|max:3',
|
||||
'r_' . AdminAcl::RESOURCE_SERVERS => 'integer|min:0|max:3',
|
||||
'r_' . AdminAcl::RESOURCE_MOUNTS => 'integer|min:0|max:3',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@ -155,6 +158,7 @@ class ApiKey extends Model
|
||||
'r_' . AdminAcl::RESOURCE_EGGS => 'int',
|
||||
'r_' . AdminAcl::RESOURCE_NODES => 'int',
|
||||
'r_' . AdminAcl::RESOURCE_SERVERS => 'int',
|
||||
'r_' . AdminAcl::RESOURCE_MOUNTS => 'int',
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ class AdminAcl
|
||||
public const RESOURCE_EGGS = 'eggs';
|
||||
public const RESOURCE_DATABASE_HOSTS = 'database_hosts';
|
||||
public const RESOURCE_SERVER_DATABASES = 'server_databases';
|
||||
public const RESOURCE_MOUNTS = 'mounts';
|
||||
|
||||
/**
|
||||
* Determine if an API key has permission to perform a specific read/write operation.
|
||||
|
89
app/Transformers/Api/Application/MountTransformer.php
Normal file
89
app/Transformers/Api/Application/MountTransformer.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Transformers\Api\Application;
|
||||
|
||||
use App\Models\Mount;
|
||||
use League\Fractal\Resource\Collection;
|
||||
use League\Fractal\Resource\NullResource;
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
|
||||
class MountTransformer extends BaseTransformer
|
||||
{
|
||||
/**
|
||||
* List of resources that can be included.
|
||||
*/
|
||||
protected array $availableIncludes = ['eggs', 'nodes', 'servers'];
|
||||
|
||||
/**
|
||||
* Return the resource name for the JSONAPI output.
|
||||
*/
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return Mount::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
public function transform(Mount $model)
|
||||
{
|
||||
return $model->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the eggs associated with this mount.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeEggs(Mount $mount): Collection|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_EGGS)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$mount->loadMissing('eggs');
|
||||
|
||||
return $this->collection(
|
||||
$mount->getRelation('eggs'),
|
||||
$this->makeTransformer(EggTransformer::class),
|
||||
'egg'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the nodes associated with this mount.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeNodes(Mount $mount): Collection|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$mount->loadMissing('nodes');
|
||||
|
||||
return $this->collection(
|
||||
$mount->getRelation('nodes'),
|
||||
$this->makeTransformer(NodeTransformer::class),
|
||||
'node'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the servers associated with this mount.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeServers(Mount $mount): Collection|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$mount->loadMissing('servers');
|
||||
|
||||
return $this->collection(
|
||||
$mount->getRelation('servers'),
|
||||
$this->makeTransformer(ServerTransformer::class),
|
||||
'server'
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('api_keys', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('r_mounts')->default(0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('api_keys', function (Blueprint $table) {
|
||||
$table->dropColumn('r_mounts');
|
||||
});
|
||||
}
|
||||
};
|
@ -52,4 +52,7 @@ return [
|
||||
'api' => [
|
||||
'resource_not_found' => 'The requested resource does not exist on this server.',
|
||||
],
|
||||
'mount' => [
|
||||
'servers_attached' => 'A mount must have no servers attached to it in order to be deleted.',
|
||||
],
|
||||
];
|
||||
|
@ -118,3 +118,26 @@ Route::group(['prefix' => '/database-hosts'], function () {
|
||||
|
||||
Route::delete('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'delete']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mount Controller Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Endpoint: /api/application/mounts
|
||||
|
|
||||
*/
|
||||
Route::prefix('mounts')->group(function () {
|
||||
Route::get('/', [Application\Mounts\MountController::class, 'index'])->name('api.application.mounts');
|
||||
Route::get('/{mount:id}', [Application\Mounts\MountController::class, 'view'])->name('api.application.mounts.view');
|
||||
|
||||
Route::post('/', [Application\Mounts\MountController::class, 'store']);
|
||||
Route::post('/{mount:id}/eggs', [Application\Mounts\MountController::class, 'addEggs'])->name('api.application.mounts.eggs');
|
||||
Route::post('/{mount:id}/nodes', [Application\Mounts\MountController::class, 'addNodes'])->name('api.application.mounts.nodes');
|
||||
|
||||
Route::patch('/{mount:id}', [Application\Mounts\MountController::class, 'update']);
|
||||
|
||||
Route::delete('/{mount:id}', [Application\Mounts\MountController::class, 'delete']);
|
||||
Route::delete('/{mount:id}/eggs/{egg_id}', [Application\Mounts\MountController::class, 'deleteEgg']);
|
||||
Route::delete('/{mount:id}/nodes/{node_id}', [Application\Mounts\MountController::class, 'deleteNode']);
|
||||
});
|
||||
|
@ -87,6 +87,7 @@ abstract class ApplicationApiIntegrationTestCase extends IntegrationTestCase
|
||||
'r_eggs' => AdminAcl::READ | AdminAcl::WRITE,
|
||||
'r_database_hosts' => AdminAcl::READ | AdminAcl::WRITE,
|
||||
'r_server_databases' => AdminAcl::READ | AdminAcl::WRITE,
|
||||
'r_mounts' => AdminAcl::READ | AdminAcl::WRITE,
|
||||
], $permissions));
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user