feat(v5): authorize creates, deep-link selection, list apps in v4
Restrict V5 application and resource-connection creation to team admins. Resolve project, environment, and application from query params and keep session selection in sync. Surface V5 apps on the v4 resource index and count them for project/environment emptiness. Create the flux data dir on install and upgrade.
This commit is contained in:
parent
772093928f
commit
a4ab69df56
23 changed files with 299 additions and 38 deletions
|
|
@ -51,6 +51,7 @@ public function __construct(private readonly ConnectionFirewallSync $firewallSyn
|
|||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$currentTeam = $this->currentTeamOrFail($request);
|
||||
$this->authorize('create', [V5Application::class, $currentTeam]);
|
||||
$projects = $this->projects($currentTeam);
|
||||
[$selectedProject, $selectedEnvironment] = $this->selectedProjectAndEnvironment($request, $projects);
|
||||
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ protected function serializeCurrentTeam(mixed $currentTeam): ?array
|
|||
*/
|
||||
protected function selectedProjectAndEnvironment(Request $request, array $projects): array
|
||||
{
|
||||
$selectedProjectUuid = $request->session()->get(self::SELECTED_PROJECT_SESSION_KEY);
|
||||
$selectedEnvironmentUuid = $request->session()->get(self::SELECTED_ENVIRONMENT_SESSION_KEY);
|
||||
$selectedProjectUuid = $request->query('project', $request->session()->get(self::SELECTED_PROJECT_SESSION_KEY));
|
||||
$selectedEnvironmentUuid = $request->query('environment', $request->session()->get(self::SELECTED_ENVIRONMENT_SESSION_KEY));
|
||||
$selectedProject = null;
|
||||
|
||||
foreach ($projects as $project) {
|
||||
|
|
@ -59,6 +59,13 @@ protected function selectedProjectAndEnvironment(Request $request, array $projec
|
|||
|
||||
$selectedEnvironment ??= $selectedProject['environments'][0] ?? null;
|
||||
|
||||
if ($request->query->has('project') || $request->query->has('environment')) {
|
||||
$request->session()->put([
|
||||
self::SELECTED_PROJECT_SESSION_KEY => $selectedProject['uuid'] ?? null,
|
||||
self::SELECTED_ENVIRONMENT_SESSION_KEY => $selectedEnvironment['uuid'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
return [$selectedProject, $selectedEnvironment];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,17 +33,23 @@ public function __invoke(Request $request, FluxHealth $fluxHealth): Response
|
|||
$currentTeam = $request->attributes->get('v5.currentTeam');
|
||||
$projects = $this->projects($currentTeam);
|
||||
[$selectedProject, $selectedEnvironment] = $this->selectedProjectAndEnvironment($request, $projects);
|
||||
$applications = $this->applications($currentTeam, $selectedProject, $selectedEnvironment);
|
||||
$requestedApplicationUuid = $request->query('application');
|
||||
$selectedApplicationUuid = collect($applications)->contains(
|
||||
fn (array $application): bool => $application['id'] === $requestedApplicationUuid
|
||||
) ? $requestedApplicationUuid : null;
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'currentTeam' => $this->serializeCurrentTeam($currentTeam),
|
||||
'flux' => $fluxHealth->check(),
|
||||
'applications' => $this->applications($currentTeam, $selectedProject, $selectedEnvironment),
|
||||
'applications' => $applications,
|
||||
'caddyIngresses' => $this->caddyIngresses($currentTeam),
|
||||
'resourceConnections' => $this->resourceConnections($currentTeam, $selectedProject, $selectedEnvironment),
|
||||
'nginxServers' => $this->nginxServers($currentTeam),
|
||||
'projects' => $projects,
|
||||
'selectedProjectUuid' => $selectedProject['uuid'] ?? null,
|
||||
'selectedEnvironmentUuid' => $selectedEnvironment['uuid'] ?? null,
|
||||
'selectedApplicationUuid' => $selectedApplicationUuid,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public function __construct(
|
|||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$currentTeam = $this->currentTeamOrFail($request);
|
||||
$this->authorize('create', [ResourceConnection::class, $currentTeam]);
|
||||
$projects = $this->projects($currentTeam);
|
||||
[$selectedProject, $selectedEnvironment] = $this->selectedProjectAndEnvironment($request, $projects);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\V5\Application as V5Application;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
|
|
@ -61,6 +62,7 @@ public function mount(): void
|
|||
->select('id', 'uuid', 'name', 'project_id')
|
||||
->with([
|
||||
'applications:id,uuid,name,environment_id',
|
||||
'v5Applications:id,uuid,name,environment_id,status',
|
||||
'services:id,uuid,name,environment_id',
|
||||
'postgresqls:id,uuid,name,environment_id',
|
||||
'redis:id,uuid,name,environment_id',
|
||||
|
|
@ -103,6 +105,23 @@ public function mount(): void
|
|||
|
||||
return $application;
|
||||
});
|
||||
$this->applications = $this->applications
|
||||
->merge(V5Application::query()
|
||||
->where('team_id', currentTeam()->id)
|
||||
->where('project_id', $this->project->id)
|
||||
->where('environment_id', $this->environment->id)
|
||||
->with('server:id,name')
|
||||
->get()
|
||||
->map(function (V5Application $application) use ($projectUuid, $environmentUuid) {
|
||||
$application->hrefLink = route('v5.dashboard', [
|
||||
'project' => $projectUuid,
|
||||
'environment' => $environmentUuid,
|
||||
'application' => $application->uuid,
|
||||
]);
|
||||
|
||||
return $application;
|
||||
}))
|
||||
->sortBy('name');
|
||||
|
||||
// Load all database resources in a single query per type
|
||||
$databaseTypes = [
|
||||
|
|
@ -180,16 +199,19 @@ private function toSearchableArray(Collection $items): array
|
|||
'uuid' => $item->uuid,
|
||||
'name' => $item->name,
|
||||
'fqdn' => $item->fqdn ?? null,
|
||||
'description' => $item->description ?? null,
|
||||
'description' => $item instanceof V5Application ? 'Managed by Coolify V5' : ($item->description ?? null),
|
||||
'status' => $item->status ?? '',
|
||||
'version' => $item instanceof V5Application ? 'v5' : 'v4',
|
||||
'server_status' => $item->server_status ?? null,
|
||||
'hrefLink' => $item->hrefLink ?? '',
|
||||
'destination' => [
|
||||
'server' => [
|
||||
'name' => $item->destination?->server?->name ?? 'Unknown',
|
||||
'name' => $item instanceof V5Application
|
||||
? ($item->server?->name ?? 'Unknown')
|
||||
: ($item->destination?->server?->name ?? 'Unknown'),
|
||||
],
|
||||
],
|
||||
'tags' => $item->tags->map(fn ($tag) => [
|
||||
'tags' => ($item instanceof V5Application ? collect() : $item->tags)->map(fn ($tag) => [
|
||||
'id' => $tag->id,
|
||||
'name' => $tag->name,
|
||||
])->values()->toArray(),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\V5\Application as V5Application;
|
||||
use App\Models\V5\ResourceConnection as V5ResourceConnection;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -54,7 +56,9 @@ public static function ownedByCurrentTeamAPI(int $teamId)
|
|||
|
||||
public function isEmpty()
|
||||
{
|
||||
return $this->applications()->count() == 0 &&
|
||||
return ! V5Application::query()->where('environment_id', $this->id)->exists() &&
|
||||
! V5ResourceConnection::query()->where('environment_id', $this->id)->exists() &&
|
||||
$this->applications()->count() == 0 &&
|
||||
$this->redis()->count() == 0 &&
|
||||
$this->postgresqls()->count() == 0 &&
|
||||
$this->mysqls()->count() == 0 &&
|
||||
|
|
@ -76,6 +80,11 @@ public function applications()
|
|||
return $this->hasMany(Application::class);
|
||||
}
|
||||
|
||||
public function v5Applications()
|
||||
{
|
||||
return $this->hasMany(V5Application::class);
|
||||
}
|
||||
|
||||
public function postgresqls()
|
||||
{
|
||||
return $this->hasMany(StandalonePostgresql::class);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\V5\Application as V5Application;
|
||||
use App\Models\V5\ResourceConnection as V5ResourceConnection;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
|
@ -144,7 +146,9 @@ public function mariadbs()
|
|||
|
||||
public function isEmpty()
|
||||
{
|
||||
return $this->applications()->count() == 0 &&
|
||||
return ! V5Application::query()->where('project_id', $this->id)->exists() &&
|
||||
! V5ResourceConnection::query()->where('project_id', $this->id)->exists() &&
|
||||
$this->applications()->count() == 0 &&
|
||||
$this->redis()->count() == 0 &&
|
||||
$this->postgresqls()->count() == 0 &&
|
||||
$this->mysqls()->count() == 0 &&
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@
|
|||
|
||||
class ApplicationPolicy
|
||||
{
|
||||
public function create(User $user, Team $team): Response
|
||||
{
|
||||
return $user->isAdminOfTeam($team->id)
|
||||
? Response::allow()
|
||||
: Response::deny('You do not have permission to manage applications in this team.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the application within the current team.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@
|
|||
|
||||
class ResourceConnectionPolicy
|
||||
{
|
||||
public function create(User $user, Team $team): Response
|
||||
{
|
||||
return $user->isAdminOfTeam($team->id)
|
||||
? Response::allow()
|
||||
: Response::deny('You do not have permission to manage resource connections in this team.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the connection within the current team.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -228,11 +228,12 @@ if [ "$WARNING_SPACE" = true ]; then
|
|||
sleep 5
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel}
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel,flux}
|
||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||
mkdir -p /data/coolify/proxy/dynamic
|
||||
|
||||
chown -R 9999:root /data/coolify
|
||||
chown -R 9999:root /data/coolify/flux
|
||||
chmod -R 700 /data/coolify
|
||||
|
||||
INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
|
||||
|
|
|
|||
|
|
@ -171,6 +171,9 @@ else
|
|||
log "Network 'coolify' already exists"
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/flux
|
||||
chown -R 9999:root /data/coolify/flux
|
||||
|
||||
# Check if Docker config file exists
|
||||
DOCKER_CONFIG_MOUNT=""
|
||||
if [ -f /root/.docker/config.json ]; then
|
||||
|
|
|
|||
58
package-lock.json
generated
58
package-lock.json
generated
|
|
@ -135,7 +135,6 @@
|
|||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
|
|
@ -740,7 +739,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
|
|
@ -789,7 +787,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
|
|
@ -997,12 +994,36 @@
|
|||
"@noble/ciphers": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz",
|
||||
"integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
|
||||
"integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
|
|
@ -1244,7 +1265,6 @@
|
|||
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
|
|
@ -2082,7 +2102,8 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
|
|
@ -2167,7 +2188,6 @@
|
|||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
|
|
@ -2178,7 +2198,6 @@
|
|||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
|
|
@ -2337,8 +2356,7 @@
|
|||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
|
|
@ -2425,6 +2443,7 @@
|
|||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -2445,6 +2464,7 @@
|
|||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
|
|
@ -2600,7 +2620,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
|
|
@ -3172,6 +3191,7 @@
|
|||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
|
@ -3200,7 +3220,8 @@
|
|||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dot-prop": {
|
||||
"version": "6.0.1",
|
||||
|
|
@ -3523,7 +3544,6 @@
|
|||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
|
|
@ -3973,7 +3993,6 @@
|
|||
"integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
|
|
@ -4871,6 +4890,7 @@
|
|||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
|
|
@ -5622,6 +5642,7 @@
|
|||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
|
|
@ -5777,7 +5798,6 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -5787,7 +5807,6 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
|
|
@ -5800,7 +5819,8 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.18.0",
|
||||
|
|
@ -6501,8 +6521,7 @@
|
|||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
|
|
@ -6718,7 +6737,6 @@
|
|||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -6841,7 +6859,6 @@
|
|||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
|
|
@ -7465,7 +7482,6 @@
|
|||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export default function Dashboard({
|
|||
projects = [],
|
||||
selectedProjectUuid = null,
|
||||
selectedEnvironmentUuid = null,
|
||||
selectedApplicationUuid = null,
|
||||
}: V5DashboardProps) {
|
||||
const [applications, setApplications] = useState<V5Application[]>(initialApplications);
|
||||
const [ingresses, setIngresses] = useState<V5CaddyIngress[]>(caddyIngresses);
|
||||
|
|
@ -175,11 +176,20 @@ export default function Dashboard({
|
|||
setIngresses(settledResources.ingresses);
|
||||
resetConnections(initialResourceConnections);
|
||||
setSelectedNginxServerId((currentServerId) => currentServerId || nginxServers[0]?.id || '');
|
||||
setSelectedApplicationId(null);
|
||||
setSelectedInspectorApplicationId(null);
|
||||
const linkedApplicationExists = settledResources.applications.some((application) => application.id === selectedApplicationUuid);
|
||||
setSelectedApplicationId(linkedApplicationExists ? selectedApplicationUuid : null);
|
||||
setSelectedInspectorApplicationId(linkedApplicationExists ? selectedApplicationUuid : null);
|
||||
centerOnCanvasNodes(settledResources.applications, settledResources.ingresses);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialApplications, caddyIngresses, initialResourceConnections, nginxServers[0]?.id, selectedProjectUuid, selectedEnvironmentUuid]);
|
||||
}, [
|
||||
initialApplications,
|
||||
caddyIngresses,
|
||||
initialResourceConnections,
|
||||
nginxServers[0]?.id,
|
||||
selectedProjectUuid,
|
||||
selectedEnvironmentUuid,
|
||||
selectedApplicationUuid,
|
||||
]);
|
||||
|
||||
useCanvasResourceMerge({
|
||||
teamId: currentTeam?.id ?? null,
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ export type V5DashboardProps = {
|
|||
projects?: V5Project[];
|
||||
selectedProjectUuid?: string | null;
|
||||
selectedEnvironmentUuid?: string | null;
|
||||
selectedApplicationUuid?: string | null;
|
||||
};
|
||||
|
||||
export type SelectItemOption = {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ class="relative w-48 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
|
|||
->merge($env->clickhouses ?? collect());
|
||||
$envResources = collect()
|
||||
->merge($env->applications->map(fn($app) => ['type' => 'application', 'resource' => $app]))
|
||||
->merge($env->v5Applications->map(fn($app) => ['type' => 'v5-application', 'resource' => $app]))
|
||||
->merge($envDatabases->map(fn($db) => ['type' => 'database', 'resource' => $db]))
|
||||
->merge($env->services->map(fn($svc) => ['type' => 'service', 'resource' => $svc]))
|
||||
->sortBy(fn($item) => strtolower($item['resource']->name));
|
||||
|
|
@ -140,6 +141,7 @@ class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-neutral-100 dark:hover
|
|||
->merge($env->clickhouses ?? collect());
|
||||
$envResources = collect()
|
||||
->merge($env->applications->map(fn($app) => ['type' => 'application', 'resource' => $app]))
|
||||
->merge($env->v5Applications->map(fn($app) => ['type' => 'v5-application', 'resource' => $app]))
|
||||
->merge($envDatabases->map(fn($db) => ['type' => 'database', 'resource' => $db]))
|
||||
->merge($env->services->map(fn($svc) => ['type' => 'service', 'resource' => $svc]));
|
||||
@endphp
|
||||
|
|
@ -157,6 +159,11 @@ class="relative w-56 bg-white dark:bg-coolgray-100 rounded-md shadow-lg py-1 bor
|
|||
$resType = $envResource['type'];
|
||||
$res = $envResource['resource'];
|
||||
$resRoute = match ($resType) {
|
||||
'v5-application' => route('v5.dashboard', [
|
||||
'project' => $project->uuid,
|
||||
'environment' => $env->uuid,
|
||||
'application' => $res->uuid,
|
||||
]),
|
||||
'application' => route('project.application.configuration', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $env->uuid,
|
||||
|
|
@ -233,10 +240,15 @@ class="font-semibold" x-text="search"></span>".</p>
|
|||
class="grid grid-cols-1 gap-4 pt-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<template x-for="item in filteredApplications" :key="item.uuid">
|
||||
<span>
|
||||
<a class="h-24 coolbox group" :href="item.hrefLink" {{ wireNavigate() }}>
|
||||
<a class="h-24 coolbox group" :href="item.hrefLink"
|
||||
@click="if (item.version === 'v5') { $event.preventDefault(); window.location.assign(item.hrefLink) }"
|
||||
{{ wireNavigate() }}>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex gap-2 px-4">
|
||||
<div class="pb-2 truncate box-title" x-text="item.name"></div>
|
||||
<template x-if="item.version === 'v5'">
|
||||
<span class="badge">V5</span>
|
||||
</template>
|
||||
<div class="flex-1"></div>
|
||||
<template x-if="item.status.startsWith('running')">
|
||||
<div title="running" class="bg-success badge-dashboard"></div>
|
||||
|
|
@ -271,9 +283,9 @@ class="flex flex-wrap gap-1 pt-1 dark:group-hover:text-white group-hover:text-bl
|
|||
<a :href="`/tags/${tag.name}`" class="tag" x-text="tag.name">
|
||||
</a>
|
||||
</template>
|
||||
<a :href="`${item.hrefLink}/tags`" class="add-tag">
|
||||
Add tag
|
||||
</a>
|
||||
<template x-if="item.version !== 'v5'">
|
||||
<a :href="`${item.hrefLink}/tags`" class="add-tag">Add tag</a>
|
||||
</template>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -228,11 +228,12 @@ if [ "$WARNING_SPACE" = true ]; then
|
|||
sleep 5
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel}
|
||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,sentinel,flux}
|
||||
mkdir -p /data/coolify/ssh/{keys,mux}
|
||||
mkdir -p /data/coolify/proxy/dynamic
|
||||
|
||||
chown -R 9999:root /data/coolify
|
||||
chown -R 9999:root /data/coolify/flux
|
||||
chmod -R 700 /data/coolify
|
||||
|
||||
INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
|
||||
|
|
|
|||
|
|
@ -171,6 +171,9 @@ else
|
|||
log "Network 'coolify' already exists"
|
||||
fi
|
||||
|
||||
mkdir -p /data/coolify/flux
|
||||
chown -R 9999:root /data/coolify/flux
|
||||
|
||||
# Fix SSH directory ownership if not owned by container user UID 9999 (fixes #6621)
|
||||
# Only changes owner — preserves existing group to respect custom setups
|
||||
SSH_OWNER=$(stat -c '%u' /data/coolify/ssh 2>/dev/null || echo "unknown")
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use App\Events\V5RealtimeTestEvent;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Models\V5\Application;
|
||||
use App\Models\V5\Application as V5Application;
|
||||
use App\Models\V5\Server as V5Server;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
|
|
@ -356,6 +357,36 @@
|
|||
->assertDontSee($otherEnvironment->uuid);
|
||||
});
|
||||
|
||||
it('opens a linked v5 application in its project and environment', function () {
|
||||
$this->withoutVite();
|
||||
fakeFluxHealth();
|
||||
createSharedUserAndTeamTables();
|
||||
|
||||
[$user, $team] = createV5UserWithTeam();
|
||||
[$project, $environment] = createV5ProjectWithEnvironment($team, 'linked-project', 'production');
|
||||
$application = Application::query()->create([
|
||||
'team_id' => $team->id,
|
||||
'project_id' => $project->id,
|
||||
'environment_id' => $environment->id,
|
||||
'created_by_user_id' => $user->id,
|
||||
'name' => 'linked-nginx',
|
||||
'image' => 'nginx:alpine',
|
||||
'container_name' => 'linked-v5-nginx',
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['currentTeam' => $team])
|
||||
->get(route('v5.dashboard', [
|
||||
'project' => $project->uuid,
|
||||
'environment' => $environment->uuid,
|
||||
'application' => $application->uuid,
|
||||
]))
|
||||
->assertSuccessful()
|
||||
->assertSee('"selectedProjectUuid":"'.$project->uuid.'"', false)
|
||||
->assertSee('"selectedEnvironmentUuid":"'.$environment->uuid.'"', false)
|
||||
->assertSee('"selectedApplicationUuid":"'.$application->uuid.'"', false);
|
||||
});
|
||||
|
||||
it('persists the selected v5 project and environment in the session', function () {
|
||||
$this->withoutVite();
|
||||
fakeFluxHealth();
|
||||
|
|
|
|||
33
tests/Feature/V5/V5CreateAuthorizationTest.php
Normal file
33
tests/Feature/V5/V5CreateAuthorizationTest.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use App\Models\V5\Application as V5Application;
|
||||
use App\Models\V5\ResourceConnection;
|
||||
|
||||
beforeEach(function () {
|
||||
resetV5DashboardTestState();
|
||||
createSharedUserAndTeamTables();
|
||||
});
|
||||
|
||||
it('prevents team members from creating v5 applications', function () {
|
||||
[$user, $team] = createV5UserWithTeam();
|
||||
$user->teams()->updateExistingPivot($team->id, ['role' => 'member']);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['currentTeam' => $team])
|
||||
->postJson('/v5/applications/nginx')
|
||||
->assertForbidden();
|
||||
|
||||
expect(V5Application::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('prevents team members from creating v5 resource connections', function () {
|
||||
[$user, $team] = createV5UserWithTeam();
|
||||
$user->teams()->updateExistingPivot($team->id, ['role' => 'member']);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['currentTeam' => $team])
|
||||
->postJson('/v5/resource-connections')
|
||||
->assertForbidden();
|
||||
|
||||
expect(ResourceConnection::query()->count())->toBe(0);
|
||||
});
|
||||
47
tests/Feature/V5/V5ParentLifecycleTest.php
Normal file
47
tests/Feature/V5/V5ParentLifecycleTest.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
use App\Models\V5\Application as V5Application;
|
||||
use App\Models\V5\ResourceConnection;
|
||||
|
||||
beforeEach(function () {
|
||||
resetV5DashboardTestState();
|
||||
createSharedUserAndTeamTables();
|
||||
});
|
||||
|
||||
it('does not consider projects or environments with v5 applications empty', function () {
|
||||
[$user, $team] = createV5UserWithTeam();
|
||||
[$project, $environment] = createV5ProjectWithEnvironment($team, 'Project', 'production');
|
||||
|
||||
V5Application::query()->create([
|
||||
'team_id' => $team->id,
|
||||
'project_id' => $project->id,
|
||||
'environment_id' => $environment->id,
|
||||
'created_by_user_id' => $user->id,
|
||||
'name' => 'nginx',
|
||||
'image' => 'nginx:alpine',
|
||||
'container_name' => 'v5-nginx',
|
||||
]);
|
||||
|
||||
expect($project->isEmpty())->toBeFalse()
|
||||
->and($environment->isEmpty())->toBeFalse();
|
||||
});
|
||||
|
||||
it('does not consider projects or environments with v5 resource connections empty', function () {
|
||||
[$user, $team] = createV5UserWithTeam();
|
||||
[$project, $environment] = createV5ProjectWithEnvironment($team, 'Project', 'production');
|
||||
|
||||
ResourceConnection::query()->create([
|
||||
'team_id' => $team->id,
|
||||
'project_id' => $project->id,
|
||||
'environment_id' => $environment->id,
|
||||
'resource_one_type' => V5Application::class,
|
||||
'resource_one_id' => 1,
|
||||
'resource_two_type' => V5Application::class,
|
||||
'resource_two_id' => 2,
|
||||
'resource_pair_key' => 'application:1|application:2',
|
||||
'created_by_user_id' => $user->id,
|
||||
]);
|
||||
|
||||
expect($project->isEmpty())->toBeFalse()
|
||||
->and($environment->isEmpty())->toBeFalse();
|
||||
});
|
||||
|
|
@ -56,6 +56,19 @@ function assertBashSyntaxIsValid(string $path): void
|
|||
'nightly upgrade' => 'other/nightly/upgrade.sh',
|
||||
]);
|
||||
|
||||
it('creates a writable flux storage directory during install and upgrade', function (string $path) {
|
||||
$script = file_get_contents(getcwd().'/'.$path);
|
||||
|
||||
expect($script)
|
||||
->toContain('/data/coolify/flux')
|
||||
->toContain('chown -R 9999:root /data/coolify/flux');
|
||||
})->with([
|
||||
'stable install' => 'scripts/install.sh',
|
||||
'nightly install' => 'other/nightly/install.sh',
|
||||
'stable upgrade' => 'scripts/upgrade.sh',
|
||||
'nightly upgrade' => 'other/nightly/upgrade.sh',
|
||||
]);
|
||||
|
||||
it('uses the selected registry url when extracting upgrade images', function (string $path) {
|
||||
$script = file_get_contents(getcwd().'/'.$path);
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,8 @@ function v5PolicyTeam(int $id = 10): Team
|
|||
$member = v5PolicyMemberUser();
|
||||
$policy = new ApplicationPolicy;
|
||||
|
||||
expect($policy->create($member, $team)->denied())->toBeTrue();
|
||||
|
||||
foreach (['update', 'updateIngress', 'delete'] as $ability) {
|
||||
$response = $policy->{$ability}($member, $application, $team);
|
||||
|
||||
|
|
@ -244,6 +246,8 @@ function v5PolicyTeam(int $id = 10): Team
|
|||
$member = v5PolicyMemberUser();
|
||||
$policy = new ResourceConnectionPolicy;
|
||||
|
||||
expect($policy->create($member, $team)->denied())->toBeTrue();
|
||||
|
||||
foreach (['update', 'delete'] as $ability) {
|
||||
$response = $policy->{$ability}($member, $connection, $team);
|
||||
|
||||
|
|
@ -251,3 +255,11 @@ function v5PolicyTeam(int $id = 10): Team
|
|||
->and($response->status())->toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('allows admins to create applications and resource connections', function () {
|
||||
$team = v5PolicyTeam(10);
|
||||
$admin = v5PolicyUser('admin');
|
||||
|
||||
expect((new ApplicationPolicy)->create($admin, $team)->allowed())->toBeTrue()
|
||||
->and((new ResourceConnectionPolicy)->create($admin, $team)->allowed())->toBeTrue();
|
||||
});
|
||||
|
|
|
|||
14
tests/Unit/V5/V4ResourceIndexV5ApplicationTest.php
Normal file
14
tests/Unit/V5/V4ResourceIndexV5ApplicationTest.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
it('includes v5 applications in the v4 resource index and links them to v5', function () {
|
||||
$component = file_get_contents(app_path('Livewire/Project/Resource/Index.php'));
|
||||
$view = file_get_contents(resource_path('views/livewire/project/resource/index.blade.php'));
|
||||
|
||||
expect($component)
|
||||
->toContain('V5Application')
|
||||
->toContain("route('v5.dashboard'")
|
||||
->toContain("? 'v5' : 'v4'")
|
||||
->and($view)
|
||||
->toContain("item.version === 'v5'")
|
||||
->toContain('V5');
|
||||
});
|
||||
Loading…
Reference in a new issue