feat(api): add REST endpoints for destinations
The Destinations section is exposed in the Coolify UI but not via the
REST API. The destination_uuid field is required when creating
applications via create-public-application, but no way to enumerate or
create destinations programmatically existed — this blocks IaC tools
(e.g. an Aspire publisher targeting Coolify).
Adds, scoped to the existing v1 auth:sanctum + ApiAllowed + api.sensitive
group:
GET /api/v1/destinations
GET /api/v1/destinations/{uuid}
DELETE /api/v1/destinations/{uuid}
GET /api/v1/servers/{server_uuid}/destinations
POST /api/v1/servers/{server_uuid}/destinations
The controller uses the existing inline-Validator convention (no Form
Request classes per the API surface's house style), reuses
StandaloneDocker::ownedByCurrentTeamAPI / SwarmDocker::ownedByCurrentTeamAPI
for team scoping (matching ScheduledTasksController etc.), and respects
the `attachedTo()` guard on delete.
No migrations needed — both standalone_dockers and swarm_dockers tables
already carry uuid/name/network/server_id/timestamps.
OpenAPI @OA\ annotations omitted in this commit to keep the diff
minimal; a follow-up can add them in the style of ServersController.
This commit is contained in:
parent
49656aa1ed
commit
fe855cf8d0
2 changed files with 113 additions and 0 deletions
105
app/Http/Controllers/Api/DestinationsController.php
Normal file
105
app/Http/Controllers/Api/DestinationsController.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class DestinationsController extends Controller
|
||||
{
|
||||
private function transform($d): array
|
||||
{
|
||||
return [
|
||||
'id' => $d->id,
|
||||
'uuid' => $d->uuid,
|
||||
'name' => $d->name,
|
||||
'network' => $d->network,
|
||||
'type' => $d instanceof SwarmDocker ? 'swarm' : 'standalone',
|
||||
'server_uuid' => $d->server?->uuid,
|
||||
'created_at' => $d->created_at,
|
||||
'updated_at' => $d->updated_at,
|
||||
];
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
$standalone = StandaloneDocker::ownedByCurrentTeamAPI($teamId)->get();
|
||||
$swarm = SwarmDocker::ownedByCurrentTeamAPI($teamId)->get();
|
||||
|
||||
return response()->json($standalone->concat($swarm)->map(fn ($d) => $this->transform($d))->values());
|
||||
}
|
||||
|
||||
public function index_by_server(Request $request, string $server_uuid)
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
$server = Server::ownedByCurrentTeamAPI($teamId)->whereUuid($server_uuid)->firstOrFail();
|
||||
$list = $server->standaloneDockers->concat($server->swarmDockers);
|
||||
|
||||
return response()->json($list->map(fn ($d) => $this->transform($d))->values());
|
||||
}
|
||||
|
||||
public function show(Request $request, string $uuid)
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
$d = StandaloneDocker::ownedByCurrentTeamAPI($teamId)->whereUuid($uuid)->first()
|
||||
?? SwarmDocker::ownedByCurrentTeamAPI($teamId)->whereUuid($uuid)->firstOrFail();
|
||||
|
||||
return response()->json($this->transform($d));
|
||||
}
|
||||
|
||||
public function create(Request $request, string $server_uuid)
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
$server = Server::ownedByCurrentTeamAPI($teamId)->whereUuid($server_uuid)->firstOrFail();
|
||||
|
||||
$allowed = ['name', 'network', 'type'];
|
||||
$extra = array_diff(array_keys($request->all()), $allowed);
|
||||
if (! empty($extra)) {
|
||||
return response()->json(['message' => 'Unknown fields', 'fields' => array_values($extra)], 422);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'nullable|string|max:255',
|
||||
'network' => ['required', 'string', 'max:255', 'regex:/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/'],
|
||||
'type' => 'nullable|in:standalone,swarm',
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
return response()->json(['message' => 'Validation failed', 'errors' => $validator->errors()], 422);
|
||||
}
|
||||
|
||||
$type = $request->input('type', 'standalone');
|
||||
$name = $request->input('name') ?: ($server->name.'-'.$request->input('network'));
|
||||
$class = $type === 'swarm' ? SwarmDocker::class : StandaloneDocker::class;
|
||||
|
||||
$exists = $class::where('server_id', $server->id)->where('network', $request->input('network'))->exists();
|
||||
if ($exists) {
|
||||
return response()->json(['message' => 'A destination with this network already exists on the server.'], 409);
|
||||
}
|
||||
|
||||
$d = $class::create([
|
||||
'name' => $name,
|
||||
'network' => $request->input('network'),
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
|
||||
return response()->json(['uuid' => $d->uuid], 201);
|
||||
}
|
||||
|
||||
public function delete(Request $request, string $uuid)
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
$d = StandaloneDocker::ownedByCurrentTeamAPI($teamId)->whereUuid($uuid)->first()
|
||||
?? SwarmDocker::ownedByCurrentTeamAPI($teamId)->whereUuid($uuid)->firstOrFail();
|
||||
if ($d->attachedTo()) {
|
||||
return response()->json(['message' => 'Destination has attached resources, detach first.'], 409);
|
||||
}
|
||||
$d->delete();
|
||||
|
||||
return response()->json(['message' => 'Deleted.']);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
use App\Http\Controllers\Api\CloudProviderTokensController;
|
||||
use App\Http\Controllers\Api\DatabasesController;
|
||||
use App\Http\Controllers\Api\DeployController;
|
||||
use App\Http\Controllers\Api\DestinationsController;
|
||||
use App\Http\Controllers\Api\GithubController;
|
||||
use App\Http\Controllers\Api\HetznerController;
|
||||
use App\Http\Controllers\Api\OtherController;
|
||||
|
|
@ -87,6 +88,13 @@
|
|||
Route::get('/servers/{uuid}/domains', [ServersController::class, 'domains_by_server'])->middleware(['api.ability:read']);
|
||||
Route::get('/servers/{uuid}/resources', [ServersController::class, 'resources_by_server'])->middleware(['api.ability:read']);
|
||||
|
||||
// Destinations — REST surface for the Coolify "Destinations" UI section (added).
|
||||
Route::get('/destinations', [DestinationsController::class, 'index'])->middleware(['api.ability:read']);
|
||||
Route::get('/destinations/{uuid}', [DestinationsController::class, 'show'])->middleware(['api.ability:read']);
|
||||
Route::delete('/destinations/{uuid}', [DestinationsController::class, 'delete'])->middleware(['api.ability:write']);
|
||||
Route::get('/servers/{server_uuid}/destinations', [DestinationsController::class, 'index_by_server'])->middleware(['api.ability:read']);
|
||||
Route::post('/servers/{server_uuid}/destinations', [DestinationsController::class, 'create'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::get('/servers/{uuid}/validate', [ServersController::class, 'validate_server'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::post('/servers', [ServersController::class, 'create_server'])->middleware(['api.ability:write']);
|
||||
|
|
|
|||
Loading…
Reference in a new issue