fix(hetzner): secure token-backed option fetches
Authorize stored Hetzner cloud provider tokens before fetching firewalls or networks, hide provider error details, and keep server creation intact if backup activation fails. Collapse advanced Hetzner server options by default while preserving visibility when advanced values are selected.
This commit is contained in:
parent
2ad11fdd9a
commit
d723a52d9c
6 changed files with 330 additions and 85 deletions
|
|
@ -543,13 +543,14 @@ public function firewalls(Request $request)
|
|||
if (! $token) {
|
||||
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
|
||||
}
|
||||
$this->authorize('view', $token);
|
||||
|
||||
try {
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
|
||||
return response()->json($hetznerService->getFirewalls());
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['message' => 'Failed to fetch firewalls: '.$e->getMessage()], 500);
|
||||
return response()->json(['message' => 'Failed to fetch Hetzner firewalls.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -637,13 +638,14 @@ public function networks(Request $request)
|
|||
if (! $token) {
|
||||
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
|
||||
}
|
||||
$this->authorize('view', $token);
|
||||
|
||||
try {
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
|
||||
return response()->json($hetznerService->getNetworks());
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json(['message' => 'Failed to fetch networks: '.$e->getMessage()], 500);
|
||||
return response()->json(['message' => 'Failed to fetch Hetzner networks.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -914,10 +916,6 @@ public function createServer(Request $request)
|
|||
// Create server on Hetzner
|
||||
$hetznerServer = $hetznerService->createServer($params);
|
||||
|
||||
if ($request->enable_backups) {
|
||||
$hetznerService->enableServerBackup((int) $hetznerServer['id']);
|
||||
}
|
||||
|
||||
// Determine IP address to use (prefer IPv4, fallback to IPv6)
|
||||
$ipAddress = null;
|
||||
if ($request->enable_ipv4 && isset($hetznerServer['public_net']['ipv4']['ip'])) {
|
||||
|
|
@ -946,6 +944,14 @@ public function createServer(Request $request)
|
|||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
|
||||
if ($request->enable_backups) {
|
||||
try {
|
||||
$hetznerService->enableServerBackup((int) $hetznerServer['id']);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate server if requested
|
||||
if ($request->instant_validate) {
|
||||
ValidateServer::dispatch($server);
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ class ByHetzner extends Component
|
|||
|
||||
public bool $enable_backups = false;
|
||||
|
||||
public bool $show_advanced_hetzner_options = false;
|
||||
|
||||
public bool $show_cloud_init_script = false;
|
||||
|
||||
public ?string $cloud_init_script = null;
|
||||
|
||||
public bool $save_cloud_init_script = false;
|
||||
|
|
@ -127,6 +131,8 @@ public function resetSelection()
|
|||
$this->save_cloud_init_script = false;
|
||||
$this->cloud_init_script_name = null;
|
||||
$this->selected_cloud_init_script_id = null;
|
||||
$this->show_advanced_hetzner_options = false;
|
||||
$this->show_cloud_init_script = false;
|
||||
$this->selectedHetznerSshKeyIds = [];
|
||||
$this->selectedHetznerFirewallIds = [];
|
||||
$this->selectedHetznerNetworkIds = [];
|
||||
|
|
@ -185,6 +191,8 @@ protected function rules(): array
|
|||
'enable_ipv4' => 'required|boolean',
|
||||
'enable_ipv6' => 'required|boolean',
|
||||
'enable_backups' => 'required|boolean',
|
||||
'show_advanced_hetzner_options' => 'boolean',
|
||||
'show_cloud_init_script' => 'boolean',
|
||||
'cloud_init_script' => ['nullable', 'string', new ValidCloudInitYaml],
|
||||
'save_cloud_init_script' => 'boolean',
|
||||
'cloud_init_script_name' => 'nullable|string|max:255',
|
||||
|
|
@ -444,6 +452,67 @@ public function getSelectedServerBackupSurchargeProperty(): ?string
|
|||
return '€'.number_format($price * 0.2, 2);
|
||||
}
|
||||
|
||||
public function getShouldShowAdvancedHetznerOptionsProperty(): bool
|
||||
{
|
||||
return $this->show_advanced_hetzner_options
|
||||
|| $this->selectedHetznerSshKeyIds !== []
|
||||
|| $this->selectedHetznerFirewallIds !== []
|
||||
|| $this->selectedHetznerNetworkIds !== []
|
||||
|| $this->enable_backups
|
||||
|| ! $this->enable_ipv4
|
||||
|| ! $this->enable_ipv6
|
||||
|| $this->show_cloud_init_script
|
||||
|| filled($this->cloud_init_script)
|
||||
|| $this->save_cloud_init_script
|
||||
|| filled($this->cloud_init_script_name)
|
||||
|| filled($this->selected_cloud_init_script_id);
|
||||
}
|
||||
|
||||
public function getAdvancedHetznerOptionsSummaryProperty(): array
|
||||
{
|
||||
$summary = [];
|
||||
|
||||
if (count($this->selectedHetznerSshKeyIds) > 0) {
|
||||
$summary[] = count($this->selectedHetznerSshKeyIds).' extra SSH '.str('key')->plural(count($this->selectedHetznerSshKeyIds));
|
||||
}
|
||||
|
||||
if (count($this->selectedHetznerFirewallIds) > 0) {
|
||||
$summary[] = count($this->selectedHetznerFirewallIds).' '.str('firewall')->plural(count($this->selectedHetznerFirewallIds));
|
||||
}
|
||||
|
||||
if (count($this->selectedHetznerNetworkIds) > 0) {
|
||||
$summary[] = count($this->selectedHetznerNetworkIds).' private '.str('network')->plural(count($this->selectedHetznerNetworkIds));
|
||||
}
|
||||
|
||||
if ($this->enable_backups) {
|
||||
$summary[] = 'Backups on';
|
||||
}
|
||||
|
||||
if (! $this->enable_ipv4 || ! $this->enable_ipv6) {
|
||||
$summary[] = collect([
|
||||
$this->enable_ipv4 ? 'IPv4' : null,
|
||||
$this->enable_ipv6 ? 'IPv6' : null,
|
||||
])->filter()->join(' + ') ?: 'No public IP';
|
||||
}
|
||||
|
||||
if ($this->show_cloud_init_script || filled($this->cloud_init_script) || filled($this->selected_cloud_init_script_id)) {
|
||||
$summary[] = 'Cloud-init';
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
public function toggleAdvancedHetznerOptions(): void
|
||||
{
|
||||
$this->show_advanced_hetzner_options = ! $this->show_advanced_hetzner_options;
|
||||
}
|
||||
|
||||
public function showCloudInitScript(): void
|
||||
{
|
||||
$this->show_cloud_init_script = true;
|
||||
$this->show_advanced_hetzner_options = true;
|
||||
}
|
||||
|
||||
public function updatedSelectedLocation($value)
|
||||
{
|
||||
// Reset server type and image when location changes
|
||||
|
|
@ -475,6 +544,15 @@ public function updatedSelectedCloudInitScriptId($value)
|
|||
$script = CloudInitScript::ownedByCurrentTeam()->findOrFail($value);
|
||||
$this->cloud_init_script = $script->script;
|
||||
$this->cloud_init_script_name = $script->name;
|
||||
$this->show_cloud_init_script = true;
|
||||
$this->show_advanced_hetzner_options = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedSaveCloudInitScript(bool $value): void
|
||||
{
|
||||
if (! $value) {
|
||||
$this->cloud_init_script_name = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -484,6 +562,7 @@ public function clearCloudInitScript()
|
|||
$this->cloud_init_script = '';
|
||||
$this->cloud_init_script_name = '';
|
||||
$this->save_cloud_init_script = false;
|
||||
$this->show_cloud_init_script = false;
|
||||
}
|
||||
|
||||
private function createHetznerServer(HetznerService $hetznerService): array
|
||||
|
|
@ -592,10 +671,6 @@ public function submit()
|
|||
// Create server on Hetzner
|
||||
$hetznerServer = $this->createHetznerServer($hetznerService);
|
||||
|
||||
if ($this->enable_backups) {
|
||||
$hetznerService->enableServerBackup((int) $hetznerServer['id']);
|
||||
}
|
||||
|
||||
// Determine IP address to use (prefer IPv4, fallback to IPv6)
|
||||
$ipAddress = null;
|
||||
if ($this->enable_ipv4 && isset($hetznerServer['public_net']['ipv4']['ip'])) {
|
||||
|
|
@ -624,6 +699,14 @@ public function submit()
|
|||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
|
||||
if ($this->enable_backups) {
|
||||
try {
|
||||
$hetznerService->enableServerBackup((int) $hetznerServer['id']);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->from_onboarding) {
|
||||
// Complete the boarding when server is successfully created via Hetzner
|
||||
currentTeam()->update([
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@
|
|||
"category": "media",
|
||||
"logo": "svgs/audiobookshelf.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-05-12T09:09:36+02:00",
|
||||
"port": "80"
|
||||
},
|
||||
"authentik": {
|
||||
|
|
@ -280,7 +280,7 @@
|
|||
"category": "monitoring",
|
||||
"logo": "svgs/beszel.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-02-21T23:17:23+05:30"
|
||||
"template_last_updated_at": "2026-04-24T02:22:08+05:30"
|
||||
},
|
||||
"beszel": {
|
||||
"documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io",
|
||||
|
|
@ -296,7 +296,7 @@
|
|||
"category": "monitoring",
|
||||
"logo": "svgs/beszel.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-02-21T23:17:23+05:30",
|
||||
"template_last_updated_at": "2026-04-24T02:21:33+05:30",
|
||||
"port": "8090"
|
||||
},
|
||||
"bitcoin-core": {
|
||||
|
|
@ -326,7 +326,7 @@
|
|||
"category": "backend",
|
||||
"logo": "svgs/bluesky.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-02-03T22:12:21+01:00",
|
||||
"template_last_updated_at": "2026-05-09T19:19:48+05:30",
|
||||
"port": "3000"
|
||||
},
|
||||
"bookstack": {
|
||||
|
|
@ -475,7 +475,7 @@
|
|||
"category": "security",
|
||||
"logo": "svgs/cap-captcha.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-04-23T01:07:14+05:30",
|
||||
"port": "3000"
|
||||
},
|
||||
"cap": {
|
||||
|
|
@ -573,7 +573,7 @@
|
|||
"category": "helpdesk",
|
||||
"logo": "svgs/chatwoot.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-06T11:35:16-05:00",
|
||||
"template_last_updated_at": "2026-05-27T09:31:29-03:00",
|
||||
"port": "3000"
|
||||
},
|
||||
"checkmate": {
|
||||
|
|
@ -704,7 +704,7 @@
|
|||
"category": "automation",
|
||||
"logo": "svgs/cloudflare-ddns.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null
|
||||
"template_last_updated_at": "2026-05-18T15:42:31+10:00"
|
||||
},
|
||||
"cloudflared": {
|
||||
"documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/?utm_source=coolify.io",
|
||||
|
|
@ -803,7 +803,7 @@
|
|||
"category": "backend",
|
||||
"logo": "svgs/convex.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-03T18:01:53+05:30",
|
||||
"template_last_updated_at": "2026-06-12T10:45:52+02:00",
|
||||
"port": "6791"
|
||||
},
|
||||
"cryptgeon": {
|
||||
|
|
@ -968,7 +968,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/docmost.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-09-27T04:00:56+02:00",
|
||||
"template_last_updated_at": "2026-05-15T13:36:02+02:00",
|
||||
"port": "3000"
|
||||
},
|
||||
"documenso": {
|
||||
|
|
@ -1248,7 +1248,7 @@
|
|||
"category": "Networking",
|
||||
"logo": "svgs/emqx-enterprise.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-04-27T09:25:48+03:00",
|
||||
"port": "18083"
|
||||
},
|
||||
"ente-photos-with-s3": {
|
||||
|
|
@ -1732,7 +1732,7 @@
|
|||
"category": "storage",
|
||||
"logo": "svgs/garage.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-12-10T12:57:34+01:00",
|
||||
"template_last_updated_at": "2026-05-31T23:57:46+02:00",
|
||||
"port": "3900"
|
||||
},
|
||||
"getoutline": {
|
||||
|
|
@ -1746,7 +1746,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/getoutline.jpeg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-12-16T14:12:15+07:00",
|
||||
"template_last_updated_at": "2026-05-09T19:19:29+05:30",
|
||||
"port": "3000"
|
||||
},
|
||||
"ghost": {
|
||||
|
|
@ -1779,7 +1779,7 @@
|
|||
"category": "devtools",
|
||||
"logo": "svgs/gitea.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null
|
||||
"template_last_updated_at": "2026-06-06T00:11:24+02:00"
|
||||
},
|
||||
"gitea-with-mariadb": {
|
||||
"documentation": "https://docs.gitea.com?utm_source=coolify.io",
|
||||
|
|
@ -2114,7 +2114,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/grocy.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00"
|
||||
"template_last_updated_at": "2026-05-12T09:10:44+02:00"
|
||||
},
|
||||
"hatchet": {
|
||||
"documentation": "https://docs.hatchet.run/self-hosting/docker-compose?utm_source=coolify.io",
|
||||
|
|
@ -2148,7 +2148,7 @@
|
|||
"category": "monitoring",
|
||||
"logo": "svgs/healthchecks.webp",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-05-25T13:06:59-04:00",
|
||||
"port": "80000"
|
||||
},
|
||||
"heimdall": {
|
||||
|
|
@ -2183,7 +2183,7 @@
|
|||
"category": "ai",
|
||||
"logo": "svgs/hermes-agent.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-05-29T12:14:17+05:30",
|
||||
"port": "8787"
|
||||
},
|
||||
"heyform": {
|
||||
|
|
@ -2218,7 +2218,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/homarr.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-10-07T12:19:45+02:00",
|
||||
"template_last_updated_at": "2026-05-09T19:30:07+05:30",
|
||||
"port": "7575"
|
||||
},
|
||||
"home-assistant": {
|
||||
|
|
@ -2361,7 +2361,7 @@
|
|||
"category": "automation",
|
||||
"logo": "svgs/inngest.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-07-02T13:25:47+02:00",
|
||||
"port": "8288"
|
||||
},
|
||||
"invoice-ninja": {
|
||||
|
|
@ -2410,7 +2410,7 @@
|
|||
"category": "media",
|
||||
"logo": "svgs/jellyfin.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-05-12T08:58:05+02:00",
|
||||
"port": "8096"
|
||||
},
|
||||
"jenkins": {
|
||||
|
|
@ -2443,7 +2443,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/jitsi.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-04-24T09:40:01+05:30",
|
||||
"port": "80"
|
||||
},
|
||||
"joomla-with-mariadb": {
|
||||
|
|
@ -2672,7 +2672,7 @@
|
|||
"category": "ai",
|
||||
"logo": "svgs/langfuse.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-03-28T16:21:57+01:00",
|
||||
"template_last_updated_at": "2026-04-23T18:08:40+02:00",
|
||||
"port": "3000"
|
||||
},
|
||||
"leantime": {
|
||||
|
|
@ -2881,7 +2881,7 @@
|
|||
"category": "auth",
|
||||
"logo": "svgs/logto_dark.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00"
|
||||
"template_last_updated_at": "2026-04-01T13:19:47Z"
|
||||
},
|
||||
"lowcoder": {
|
||||
"documentation": "https://docs.lowcoder.cloud/?utm_source=coolify.io",
|
||||
|
|
@ -3021,7 +3021,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/mealie.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-05-12T09:22:15+02:00",
|
||||
"port": "9000"
|
||||
},
|
||||
"mediawiki": {
|
||||
|
|
@ -3712,7 +3712,7 @@
|
|||
"category": "email",
|
||||
"logo": "svgs/openarchiver.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-06T11:35:16-05:00",
|
||||
"template_last_updated_at": "2026-05-09T19:35:15+05:30",
|
||||
"port": "3000"
|
||||
},
|
||||
"open-webui": {
|
||||
|
|
@ -3770,7 +3770,7 @@
|
|||
"category": "monitoring",
|
||||
"logo": "svgs/openobserve.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-05-19T16:40:18+05:30",
|
||||
"port": "5080"
|
||||
},
|
||||
"openpanel": {
|
||||
|
|
@ -3891,7 +3891,7 @@
|
|||
"category": "storage",
|
||||
"logo": "svgs/owncloud.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-06-02T12:12:42+03:00",
|
||||
"port": "8080"
|
||||
},
|
||||
"pairdrop": {
|
||||
|
|
@ -4075,7 +4075,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/plane.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-02-24T02:34:35+05:30",
|
||||
"template_last_updated_at": "2026-04-24T00:12:17+05:30",
|
||||
"port": "80"
|
||||
},
|
||||
"plex": {
|
||||
|
|
@ -4351,7 +4351,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/rallly.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-03-19T16:11:50+01:00",
|
||||
"port": "3000"
|
||||
},
|
||||
"reactive-resume": {
|
||||
|
|
@ -4502,7 +4502,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/ryot.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-05-12T08:34:57+02:00",
|
||||
"port": "8000"
|
||||
},
|
||||
"satisfactory": {
|
||||
|
|
@ -5176,7 +5176,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/twenty.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-06T11:35:16-05:00",
|
||||
"template_last_updated_at": "2026-04-16T23:18:19+05:30",
|
||||
"port": "3000"
|
||||
},
|
||||
"typesense": {
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@
|
|||
"category": "media",
|
||||
"logo": "svgs/audiobookshelf.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-05-12T09:09:36+02:00",
|
||||
"port": "80"
|
||||
},
|
||||
"authentik": {
|
||||
|
|
@ -280,7 +280,7 @@
|
|||
"category": "monitoring",
|
||||
"logo": "svgs/beszel.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-02-21T23:17:23+05:30"
|
||||
"template_last_updated_at": "2026-04-24T02:22:08+05:30"
|
||||
},
|
||||
"beszel": {
|
||||
"documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io",
|
||||
|
|
@ -296,7 +296,7 @@
|
|||
"category": "monitoring",
|
||||
"logo": "svgs/beszel.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-02-21T23:17:23+05:30",
|
||||
"template_last_updated_at": "2026-04-24T02:21:33+05:30",
|
||||
"port": "8090"
|
||||
},
|
||||
"bitcoin-core": {
|
||||
|
|
@ -326,7 +326,7 @@
|
|||
"category": "backend",
|
||||
"logo": "svgs/bluesky.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-02-03T22:12:21+01:00",
|
||||
"template_last_updated_at": "2026-05-09T19:19:48+05:30",
|
||||
"port": "3000"
|
||||
},
|
||||
"bookstack": {
|
||||
|
|
@ -475,7 +475,7 @@
|
|||
"category": "security",
|
||||
"logo": "svgs/cap-captcha.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-04-23T01:07:14+05:30",
|
||||
"port": "3000"
|
||||
},
|
||||
"cap": {
|
||||
|
|
@ -573,7 +573,7 @@
|
|||
"category": "helpdesk",
|
||||
"logo": "svgs/chatwoot.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-06T11:35:16-05:00",
|
||||
"template_last_updated_at": "2026-05-27T09:31:29-03:00",
|
||||
"port": "3000"
|
||||
},
|
||||
"checkmate": {
|
||||
|
|
@ -704,7 +704,7 @@
|
|||
"category": "automation",
|
||||
"logo": "svgs/cloudflare-ddns.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null
|
||||
"template_last_updated_at": "2026-05-18T15:42:31+10:00"
|
||||
},
|
||||
"cloudflared": {
|
||||
"documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/?utm_source=coolify.io",
|
||||
|
|
@ -803,7 +803,7 @@
|
|||
"category": "backend",
|
||||
"logo": "svgs/convex.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-03T18:01:53+05:30",
|
||||
"template_last_updated_at": "2026-06-12T10:45:52+02:00",
|
||||
"port": "6791"
|
||||
},
|
||||
"cryptgeon": {
|
||||
|
|
@ -968,7 +968,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/docmost.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-09-27T04:00:56+02:00",
|
||||
"template_last_updated_at": "2026-05-15T13:36:02+02:00",
|
||||
"port": "3000"
|
||||
},
|
||||
"documenso": {
|
||||
|
|
@ -1248,7 +1248,7 @@
|
|||
"category": "Networking",
|
||||
"logo": "svgs/emqx-enterprise.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-04-27T09:25:48+03:00",
|
||||
"port": "18083"
|
||||
},
|
||||
"ente-photos-with-s3": {
|
||||
|
|
@ -1732,7 +1732,7 @@
|
|||
"category": "storage",
|
||||
"logo": "svgs/garage.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-12-10T12:57:34+01:00",
|
||||
"template_last_updated_at": "2026-05-31T23:57:46+02:00",
|
||||
"port": "3900"
|
||||
},
|
||||
"getoutline": {
|
||||
|
|
@ -1746,7 +1746,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/getoutline.jpeg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-12-16T14:12:15+07:00",
|
||||
"template_last_updated_at": "2026-05-09T19:19:29+05:30",
|
||||
"port": "3000"
|
||||
},
|
||||
"ghost": {
|
||||
|
|
@ -1779,7 +1779,7 @@
|
|||
"category": "devtools",
|
||||
"logo": "svgs/gitea.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null
|
||||
"template_last_updated_at": "2026-06-06T00:11:24+02:00"
|
||||
},
|
||||
"gitea-with-mariadb": {
|
||||
"documentation": "https://docs.gitea.com?utm_source=coolify.io",
|
||||
|
|
@ -2114,7 +2114,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/grocy.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00"
|
||||
"template_last_updated_at": "2026-05-12T09:10:44+02:00"
|
||||
},
|
||||
"hatchet": {
|
||||
"documentation": "https://docs.hatchet.run/self-hosting/docker-compose?utm_source=coolify.io",
|
||||
|
|
@ -2148,7 +2148,7 @@
|
|||
"category": "monitoring",
|
||||
"logo": "svgs/healthchecks.webp",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-05-25T13:06:59-04:00",
|
||||
"port": "80000"
|
||||
},
|
||||
"heimdall": {
|
||||
|
|
@ -2183,7 +2183,7 @@
|
|||
"category": "ai",
|
||||
"logo": "svgs/hermes-agent.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-05-29T12:14:17+05:30",
|
||||
"port": "8787"
|
||||
},
|
||||
"heyform": {
|
||||
|
|
@ -2218,7 +2218,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/homarr.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-10-07T12:19:45+02:00",
|
||||
"template_last_updated_at": "2026-05-09T19:30:07+05:30",
|
||||
"port": "7575"
|
||||
},
|
||||
"home-assistant": {
|
||||
|
|
@ -2361,7 +2361,7 @@
|
|||
"category": "automation",
|
||||
"logo": "svgs/inngest.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-07-02T13:25:47+02:00",
|
||||
"port": "8288"
|
||||
},
|
||||
"invoice-ninja": {
|
||||
|
|
@ -2410,7 +2410,7 @@
|
|||
"category": "media",
|
||||
"logo": "svgs/jellyfin.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-05-12T08:58:05+02:00",
|
||||
"port": "8096"
|
||||
},
|
||||
"jenkins": {
|
||||
|
|
@ -2443,7 +2443,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/jitsi.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-04-24T09:40:01+05:30",
|
||||
"port": "80"
|
||||
},
|
||||
"joomla-with-mariadb": {
|
||||
|
|
@ -2672,7 +2672,7 @@
|
|||
"category": "ai",
|
||||
"logo": "svgs/langfuse.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-03-28T16:21:57+01:00",
|
||||
"template_last_updated_at": "2026-04-23T18:08:40+02:00",
|
||||
"port": "3000"
|
||||
},
|
||||
"leantime": {
|
||||
|
|
@ -2881,7 +2881,7 @@
|
|||
"category": "auth",
|
||||
"logo": "svgs/logto_dark.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00"
|
||||
"template_last_updated_at": "2026-04-01T13:19:47Z"
|
||||
},
|
||||
"lowcoder": {
|
||||
"documentation": "https://docs.lowcoder.cloud/?utm_source=coolify.io",
|
||||
|
|
@ -3021,7 +3021,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/mealie.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-05-12T09:22:15+02:00",
|
||||
"port": "9000"
|
||||
},
|
||||
"mediawiki": {
|
||||
|
|
@ -3712,7 +3712,7 @@
|
|||
"category": "email",
|
||||
"logo": "svgs/openarchiver.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-06T11:35:16-05:00",
|
||||
"template_last_updated_at": "2026-05-09T19:35:15+05:30",
|
||||
"port": "3000"
|
||||
},
|
||||
"open-webui": {
|
||||
|
|
@ -3770,7 +3770,7 @@
|
|||
"category": "monitoring",
|
||||
"logo": "svgs/openobserve.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": null,
|
||||
"template_last_updated_at": "2026-05-19T16:40:18+05:30",
|
||||
"port": "5080"
|
||||
},
|
||||
"openpanel": {
|
||||
|
|
@ -3891,7 +3891,7 @@
|
|||
"category": "storage",
|
||||
"logo": "svgs/owncloud.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-06-02T12:12:42+03:00",
|
||||
"port": "8080"
|
||||
},
|
||||
"pairdrop": {
|
||||
|
|
@ -4075,7 +4075,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/plane.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-02-24T02:34:35+05:30",
|
||||
"template_last_updated_at": "2026-04-24T00:12:17+05:30",
|
||||
"port": "80"
|
||||
},
|
||||
"plex": {
|
||||
|
|
@ -4351,7 +4351,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/rallly.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-03-19T16:11:50+01:00",
|
||||
"port": "3000"
|
||||
},
|
||||
"reactive-resume": {
|
||||
|
|
@ -4502,7 +4502,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/ryot.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2025-08-17T18:23:57+02:00",
|
||||
"template_last_updated_at": "2026-05-12T08:34:57+02:00",
|
||||
"port": "8000"
|
||||
},
|
||||
"satisfactory": {
|
||||
|
|
@ -5176,7 +5176,7 @@
|
|||
"category": "productivity",
|
||||
"logo": "svgs/twenty.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-04-06T11:35:16-05:00",
|
||||
"template_last_updated_at": "2026-04-16T23:18:19+05:30",
|
||||
"port": "3000"
|
||||
},
|
||||
"typesense": {
|
||||
|
|
|
|||
|
|
@ -278,6 +278,27 @@
|
|||
$response->assertJsonCount(2);
|
||||
$response->assertJsonFragment(['name' => 'web-firewall']);
|
||||
});
|
||||
|
||||
test('member read token cannot use a stored cloud provider token', function () {
|
||||
$member = User::factory()->create();
|
||||
$this->team->members()->attach($member->id, ['role' => 'member']);
|
||||
session(['currentTeam' => $this->team]);
|
||||
$memberToken = $member->createToken('member-read', ['read'])->plainTextToken;
|
||||
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/firewalls*' => Http::response([
|
||||
'firewalls' => [['id' => 38, 'name' => 'web-firewall']],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$memberToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->getJson('/api/v1/hetzner/firewalls?cloud_provider_token_id='.$this->hetznerToken->uuid);
|
||||
|
||||
$response->assertForbidden();
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/hetzner/networks', function () {
|
||||
|
|
@ -301,6 +322,27 @@
|
|||
$response->assertJsonCount(2);
|
||||
$response->assertJsonFragment(['name' => 'private-eu']);
|
||||
});
|
||||
|
||||
test('member read token cannot use a stored cloud provider token', function () {
|
||||
$member = User::factory()->create();
|
||||
$this->team->members()->attach($member->id, ['role' => 'member']);
|
||||
session(['currentTeam' => $this->team]);
|
||||
$memberToken = $member->createToken('member-read', ['read'])->plainTextToken;
|
||||
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/networks*' => Http::response([
|
||||
'networks' => [['id' => 456, 'name' => 'private-eu']],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$memberToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->getJson('/api/v1/hetzner/networks?cloud_provider_token_id='.$this->hetznerToken->uuid);
|
||||
|
||||
$response->assertForbidden();
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/servers/hetzner', function () {
|
||||
|
|
@ -411,6 +453,67 @@
|
|||
&& $request->url() === 'https://api.hetzner.cloud/v1/servers/456/actions/enable_backup');
|
||||
});
|
||||
|
||||
test('registers server when backup enablement fails after Hetzner creation', function () {
|
||||
Http::fake(function (HttpRequest $request) {
|
||||
if ($request->method() === 'GET' && str_starts_with($request->url(), 'https://api.hetzner.cloud/v1/ssh_keys')) {
|
||||
return Http::response([
|
||||
'ssh_keys' => [],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200);
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST' && $request->url() === 'https://api.hetzner.cloud/v1/ssh_keys') {
|
||||
return Http::response([
|
||||
'ssh_key' => ['id' => 123, 'fingerprint' => 'aa:bb:cc:dd'],
|
||||
], 201);
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST' && $request->url() === 'https://api.hetzner.cloud/v1/servers') {
|
||||
return Http::response([
|
||||
'server' => [
|
||||
'id' => 456,
|
||||
'name' => 'test-server',
|
||||
'public_net' => [
|
||||
'ipv4' => ['ip' => '1.2.3.4'],
|
||||
'ipv6' => ['ip' => '2001:db8::1'],
|
||||
],
|
||||
],
|
||||
], 201);
|
||||
}
|
||||
|
||||
if ($request->method() === 'POST' && $request->url() === 'https://api.hetzner.cloud/v1/servers/456/actions/enable_backup') {
|
||||
return Http::response([
|
||||
'error' => ['message' => 'backup unavailable'],
|
||||
], 500);
|
||||
}
|
||||
|
||||
return Http::response([], 404);
|
||||
});
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->postJson('/api/v1/servers/hetzner', [
|
||||
'cloud_provider_token_id' => $this->hetznerToken->uuid,
|
||||
'location' => 'nbg1',
|
||||
'server_type' => 'cx11',
|
||||
'image' => 15512617,
|
||||
'name' => 'test-server',
|
||||
'private_key_uuid' => $this->privateKey->uuid,
|
||||
'enable_ipv4' => true,
|
||||
'enable_ipv6' => true,
|
||||
'enable_backups' => true,
|
||||
]);
|
||||
|
||||
$response->assertCreated();
|
||||
$response->assertJsonFragment(['hetzner_server_id' => 456, 'ip' => '1.2.3.4']);
|
||||
$this->assertDatabaseHas('servers', [
|
||||
'name' => 'test-server',
|
||||
'team_id' => $this->team->id,
|
||||
'hetzner_server_id' => 456,
|
||||
]);
|
||||
});
|
||||
|
||||
test('generates server name if not provided', function () {
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/ssh_keys' => Http::response([
|
||||
|
|
@ -711,4 +814,38 @@
|
|||
$response->assertExactJson(['message' => 'Failed to fetch Hetzner SSH keys.']);
|
||||
expect($response->getContent())->not->toContain('INTERNAL_LEAK_TOKEN_abc');
|
||||
});
|
||||
|
||||
test('firewalls endpoint returns generic 500 message on upstream failure', function () {
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/firewalls*' => Http::response([
|
||||
'error' => ['message' => 'INTERNAL_LEAK_TOKEN_abc'],
|
||||
], 500),
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->getJson('/api/v1/hetzner/firewalls?cloud_provider_token_id='.$this->hetznerToken->uuid);
|
||||
|
||||
$response->assertStatus(500);
|
||||
$response->assertExactJson(['message' => 'Failed to fetch Hetzner firewalls.']);
|
||||
expect($response->getContent())->not->toContain('INTERNAL_LEAK_TOKEN_abc');
|
||||
});
|
||||
|
||||
test('networks endpoint returns generic 500 message on upstream failure', function () {
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/networks*' => Http::response([
|
||||
'error' => ['message' => 'INTERNAL_LEAK_TOKEN_abc'],
|
||||
], 500),
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->getJson('/api/v1/hetzner/networks?cloud_provider_token_id='.$this->hetznerToken->uuid);
|
||||
|
||||
$response->assertStatus(500);
|
||||
$response->assertExactJson(['message' => 'Failed to fetch Hetzner networks.']);
|
||||
expect($response->getContent())->not->toContain('INTERNAL_LEAK_TOKEN_abc');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -144,15 +144,6 @@
|
|||
->and(count($sshKeys))->toBe(3);
|
||||
});
|
||||
|
||||
it('validates network array merging removes duplicate Hetzner network ids', function () {
|
||||
$selectedHetznerNetworkIds = [456, 789, 456];
|
||||
|
||||
$networkIds = array_values(array_unique($selectedHetznerNetworkIds));
|
||||
|
||||
expect($networkIds)->toBe([456, 789])
|
||||
->and(count($networkIds))->toBe(2);
|
||||
});
|
||||
|
||||
describe('Boarding Flow Integration', function () {
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
|
|
@ -171,7 +162,7 @@
|
|||
|
||||
test('completes boarding when server is created from onboarding', function () {
|
||||
// Verify boarding is initially enabled
|
||||
expect($this->team->fresh()->show_boarding)->toBeTrue();
|
||||
expect((bool) $this->team->fresh()->show_boarding)->toBeTrue();
|
||||
|
||||
// Mount the component with from_onboarding flag
|
||||
$component = Livewire::test(ByHetzner::class)
|
||||
|
|
@ -188,23 +179,51 @@
|
|||
|
||||
test('boarding flag remains unchanged when not from onboarding', function () {
|
||||
// Verify boarding is initially enabled
|
||||
expect($this->team->fresh()->show_boarding)->toBeTrue();
|
||||
expect((bool) $this->team->fresh()->show_boarding)->toBeTrue();
|
||||
|
||||
// Mount the component without from_onboarding flag (default false)
|
||||
Livewire::test(ByHetzner::class)
|
||||
->set('from_onboarding', false);
|
||||
|
||||
// Boarding should still be enabled since it wasn't created from onboarding
|
||||
expect($this->team->fresh()->show_boarding)->toBeTrue();
|
||||
expect((bool) $this->team->fresh()->show_boarding)->toBeTrue();
|
||||
});
|
||||
|
||||
test('shows the backups option with the pricing note in the create dialog', function () {
|
||||
test('keeps advanced Hetzner options collapsed by default in the create dialog', function () {
|
||||
Livewire::test(ByHetzner::class)
|
||||
->set('current_step', 2)
|
||||
->assertSet('enable_backups', false)
|
||||
->assertSet('show_advanced_hetzner_options', false)
|
||||
->assertSee('Advanced Hetzner options')
|
||||
->assertDontSee('Extra SSH Keys')
|
||||
->assertDontSee('Firewalls')
|
||||
->assertDontSee('Private Networks')
|
||||
->assertDontSee('Enable Hetzner Backups')
|
||||
->assertDontSee('Cloud-Init Script');
|
||||
});
|
||||
|
||||
test('shows advanced Hetzner options when expanded', function () {
|
||||
Livewire::test(ByHetzner::class)
|
||||
->set('current_step', 2)
|
||||
->set('show_advanced_hetzner_options', true)
|
||||
->assertSee('Extra SSH Keys')
|
||||
->assertSee('Firewalls')
|
||||
->assertSee('Private Networks')
|
||||
->assertSee('Enable Hetzner Backups')
|
||||
->assertSee('Add cloud-init script')
|
||||
->assertSee('additional 20% of the server monthly fee');
|
||||
});
|
||||
|
||||
test('shows the cloud init script name only when saving the script', function () {
|
||||
Livewire::test(ByHetzner::class)
|
||||
->set('current_step', 2)
|
||||
->set('show_advanced_hetzner_options', true)
|
||||
->set('show_cloud_init_script', true)
|
||||
->assertSee('Cloud-Init Script')
|
||||
->assertSee('Save this script for later use')
|
||||
->assertDontSee('Script name...')
|
||||
->set('save_cloud_init_script', true)
|
||||
->assertSee('Script name...');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hetzner data loading', function () {
|
||||
|
|
@ -239,7 +258,7 @@
|
|||
], 200),
|
||||
'https://api.hetzner.cloud/v1/server_types*' => Http::response([
|
||||
'server_types' => [
|
||||
['id' => 1, 'name' => 'cx11', 'description' => 'CX11', 'locations' => [['name' => 'nbg1']], 'architecture' => 'x86'],
|
||||
['id' => 1, 'name' => 'cx11', 'description' => 'CX11', 'cores' => 1, 'memory' => 2.0, 'disk' => 20, 'locations' => [['name' => 'nbg1']], 'architecture' => 'x86'],
|
||||
],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200),
|
||||
|
|
|
|||
Loading…
Reference in a new issue