mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 18:23:40 +00:00
feat(server): fall back Sentinel URL to request host
Use the current request scheme/host when no FQDN or public IP is set, skipping loopback hosts. Polish boarding/server create cards, optional listbox tooltips, and cover the layout and Sentinel URL behavior in tests.
This commit is contained in:
@@ -228,7 +228,7 @@ class ServerSetting extends Model
|
||||
}
|
||||
|
||||
if (blank($url)) {
|
||||
throw new \RuntimeException('Set an instance FQDN or public IP before enabling Sentinel.');
|
||||
throw new \RuntimeException('Set an instance FQDN, public IP, or reachable Coolify URL before enabling Sentinel.');
|
||||
}
|
||||
|
||||
return $url;
|
||||
@@ -246,6 +246,8 @@ class ServerSetting extends Model
|
||||
$domain = 'http://'.$settings->public_ipv4.':8000';
|
||||
} elseif ($settings->public_ipv6) {
|
||||
$domain = 'http://'.$settings->public_ipv6.':8000';
|
||||
} else {
|
||||
$domain = $this->sentinelUrlFromCurrentRequest();
|
||||
}
|
||||
$this->sentinel_custom_url = $domain;
|
||||
if ($save) {
|
||||
@@ -259,6 +261,29 @@ class ServerSetting extends Model
|
||||
return $domain;
|
||||
}
|
||||
|
||||
private function sentinelUrlFromCurrentRequest(): ?string
|
||||
{
|
||||
if (! app()->bound('request')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$request = request();
|
||||
$host = strtolower($request->getHost());
|
||||
|
||||
if (
|
||||
$host === 'localhost' ||
|
||||
str_ends_with($host, '.localhost') ||
|
||||
$host === '::1' ||
|
||||
$host === '::' ||
|
||||
$host === '0.0.0.0' ||
|
||||
str_starts_with($host, '127.')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $request->getSchemeAndHttpHost();
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
'wire' => true, // false = purely client-side value (no Livewire binding)
|
||||
'value' => null, // initial value when wire=false
|
||||
'disabled' => false,
|
||||
'tooltip' => true,
|
||||
])
|
||||
|
||||
<div class="w-full min-w-0">
|
||||
@@ -53,7 +54,7 @@
|
||||
@click.outside="open = false" @keydown.escape="open = false">
|
||||
<button id="{{ $id }}-trigger" type="button" class="listbox-trigger" @click="open = !open"
|
||||
@disabled($disabled) {{ $attributes->whereStartsWith('x-bind:disabled') }} aria-haspopup="listbox"
|
||||
:aria-expanded="open" :title="current">
|
||||
:aria-expanded="open" @if ($tooltip) :title="current" @endif>
|
||||
<span class="listbox-trigger-label" x-text="current"></span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" class="size-3.5 shrink-0 opacity-60">
|
||||
|
||||
@@ -97,8 +97,12 @@
|
||||
<x-slot:actions>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 w-full">
|
||||
<button
|
||||
class="group relative min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left transition-colors hover:border-coollabs/35 hover:bg-coollabs/[0.03] dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-warning/25 dark:hover:bg-warning/[0.04]"
|
||||
class="group relative cursor-pointer min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
wire:target="setServerType('localhost')" wire:click="setServerType('localhost')">
|
||||
<span role="button" tabindex="0" aria-label="About this machine"
|
||||
data-tooltip="The machine running Coolify. Not recommended for production workloads due to resource contention."
|
||||
@click.stop @keydown.enter.stop @keydown.space.prevent.stop
|
||||
class="absolute top-3 right-3 flex size-6 items-center justify-center rounded-full border border-neutral-200 text-[11px] font-semibold text-neutral-500 hover:border-coollabs/35 hover:text-coollabs dark:border-white/[0.1] dark:text-fg-dim dark:hover:border-warning/30 dark:hover:text-warning">i</span>
|
||||
<div class="flex flex-col gap-4 text-left">
|
||||
<svg class="size-10" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
@@ -117,8 +121,12 @@
|
||||
|
||||
|
||||
<button
|
||||
class="group relative min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left transition-colors hover:border-coollabs/35 hover:bg-coollabs/[0.03] dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-warning/25 dark:hover:bg-warning/[0.04]"
|
||||
class="group relative cursor-pointer min-h-36 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
wire:target="setServerType('remote')" wire:click="setServerType('remote')">
|
||||
<span role="button" tabindex="0" aria-label="About remote servers"
|
||||
data-tooltip="Any SSH-accessible server, including cloud VPS, bare metal, and self-hosted infrastructure."
|
||||
@click.stop @keydown.enter.stop @keydown.space.prevent.stop
|
||||
class="absolute top-3 right-3 flex size-6 items-center justify-center rounded-full border border-neutral-200 text-[11px] font-semibold text-neutral-500 hover:border-coollabs/35 hover:text-coollabs dark:border-white/[0.1] dark:text-fg-dim dark:hover:border-warning/30 dark:hover:text-warning">i</span>
|
||||
<div class="flex flex-col gap-4 text-left">
|
||||
<svg class="size-10" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
@@ -138,7 +146,7 @@
|
||||
<x-modal-input title="Connect a Hetzner Server" isFullWidth>
|
||||
<x-slot:content>
|
||||
<div
|
||||
class="group relative flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left transition-colors hover:border-coollabs/35 hover:bg-coollabs/[0.03] dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-warning/25 dark:hover:bg-warning/[0.04]">
|
||||
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
<div class="flex h-full flex-col gap-4 text-left">
|
||||
<svg class="size-10 shrink-0" viewBox="0 0 200 200"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -160,7 +168,7 @@
|
||||
<x-modal-input title="Connect a Vultr Server" isFullWidth>
|
||||
<x-slot:content>
|
||||
<div
|
||||
class="group relative flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left transition-colors hover:border-coollabs/35 hover:bg-coollabs/[0.03] dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-warning/25 dark:hover:bg-warning/[0.04]">
|
||||
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
<div class="flex h-full flex-col gap-4 text-left">
|
||||
<svg class="size-10 shrink-0" viewBox="0 0 200 200"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -179,6 +187,23 @@
|
||||
</x-slot:content>
|
||||
<livewire:server.new.by-vultr :limit_reached="false" :from_onboarding="true" />
|
||||
</x-modal-input>
|
||||
<x-modal-input title="Connect a DigitalOcean Server" isFullWidth>
|
||||
<x-slot:content>
|
||||
<div
|
||||
class="group relative cursor-pointer flex h-full min-h-36 flex-col rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
|
||||
<div class="flex h-full flex-col gap-4 text-left">
|
||||
<x-digital-ocean-icon class="size-10 shrink-0" />
|
||||
<div class="min-h-0 flex-1">
|
||||
<h3 class="mb-1 text-[14px] font-semibold">DigitalOcean</h3>
|
||||
<p class="text-sm dark:text-neutral-400">
|
||||
Deploy servers directly from your DigitalOcean account.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot:content>
|
||||
<livewire:server.new.by-digital-ocean :limit_reached="false" :from_onboarding="true" />
|
||||
</x-modal-input>
|
||||
@endif
|
||||
@endcan
|
||||
</div>
|
||||
@@ -229,20 +254,6 @@
|
||||
</div>
|
||||
@endif
|
||||
</x-slot:actions>
|
||||
<x-slot:explanation>
|
||||
<p>
|
||||
<x-highlighted text="Servers" /> host your applications, databases, and services (collectively
|
||||
called resources). All CPU-intensive operations run on the target server.
|
||||
</p>
|
||||
<p>
|
||||
<x-highlighted text="Localhost:" /> The machine running Coolify. Not recommended for production
|
||||
workloads due to resource contention.
|
||||
</p>
|
||||
<p>
|
||||
<x-highlighted text="Remote Server:" /> Any SSH-accessible server: cloud providers (AWS, Hetzner,
|
||||
DigitalOcean), bare metal, or self-hosted infrastructure.
|
||||
</p>
|
||||
</x-slot:explanation>
|
||||
</x-boarding-step>
|
||||
@elseif ($currentState === 'private-key')
|
||||
<x-boarding-progress :currentStep="2" />
|
||||
@@ -268,7 +279,7 @@
|
||||
class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<div class="min-w-0 flex-1">
|
||||
<x-forms.listbox id="selectedExistingPrivateKey"
|
||||
label="Existing SSH key" :options="$privateKeyOptions" />
|
||||
label="Existing SSH key" :options="$privateKeyOptions" :tooltip="false" />
|
||||
</div>
|
||||
<x-forms.button type="submit">Use selected key</x-forms.button>
|
||||
</form>
|
||||
@@ -288,7 +299,7 @@
|
||||
@endif
|
||||
<div class="grid w-full grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
<button type="button"
|
||||
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left transition-colors hover:border-coollabs/35 hover:bg-coollabs/[0.03] dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-warning/25 dark:hover:bg-warning/[0.04]"
|
||||
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
wire:target="setPrivateKey('own')" wire:click="setPrivateKey('own')">
|
||||
<span
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
|
||||
@@ -302,7 +313,7 @@
|
||||
</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left transition-colors hover:border-coollabs/35 hover:bg-coollabs/[0.03] dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-warning/25 dark:hover:bg-warning/[0.04]"
|
||||
class="group flex h-full min-h-28 items-start gap-3 rounded-[10px] border border-neutral-200 bg-white p-4 text-left shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]"
|
||||
wire:target="setPrivateKey('create')" wire:click="setPrivateKey('create')">
|
||||
<span
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
|
||||
@@ -446,21 +457,6 @@
|
||||
<x-forms.button type="submit" class="w-full lg:w-auto">Validate Connection</x-forms.button>
|
||||
</form>
|
||||
</x-slot:actions>
|
||||
<x-slot:explanation>
|
||||
<p>
|
||||
<x-highlighted text="Connection Requirements:" /> Server must be accessible via SSH on the
|
||||
specified port (default 22).
|
||||
</p>
|
||||
<p>
|
||||
<x-highlighted text="Hostname Resolution:" /> Use IP addresses for direct connections or ensure
|
||||
DNS resolution is configured.
|
||||
</p>
|
||||
<p>
|
||||
<x-highlighted text="User Permissions:" /> Root or sudo-enabled users recommended for full
|
||||
Docker
|
||||
management capabilities.
|
||||
</p>
|
||||
</x-slot:explanation>
|
||||
</x-boarding-step>
|
||||
@elseif ($currentState === 'validate-server')
|
||||
<x-boarding-progress :currentStep="2" />
|
||||
@@ -582,22 +578,6 @@
|
||||
@endif
|
||||
</div>
|
||||
</x-slot:actions>
|
||||
<x-slot:explanation>
|
||||
<p>
|
||||
<x-highlighted text="Project Organization:" /> Group related resources (apps, databases,
|
||||
services)
|
||||
into logical projects.
|
||||
</p>
|
||||
<p>
|
||||
<x-highlighted text="Environments:" /> Each project includes a production environment by
|
||||
default.
|
||||
Add staging, development, or custom environments as needed.
|
||||
</p>
|
||||
<p>
|
||||
<x-highlighted text="Team Access:" /> Projects inherit team permissions and can be managed
|
||||
collaboratively.
|
||||
</p>
|
||||
</x-slot:explanation>
|
||||
</x-boarding-step>
|
||||
@elseif ($currentState === 'create-resource')
|
||||
<x-boarding-progress :currentStep="3" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
New Server | Coolify
|
||||
</x-slot>
|
||||
|
||||
<div class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="mb-5 flex min-h-9 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h1 class="min-w-0 text-[24px]! leading-7! font-semibold! tracking-tight!">New server</h1>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@if ($selectedType)
|
||||
@@ -11,27 +11,6 @@
|
||||
Change method
|
||||
</a>
|
||||
@endif
|
||||
@if ($selectedType && $selectedType !== 'manual' && ! $selectedTokenUuid)
|
||||
@php
|
||||
$tokenProvider = $selectedType === 'digital-ocean' ? 'digitalocean' : $selectedType;
|
||||
$tokenProviderName = $selectedType === 'digital-ocean'
|
||||
? 'DigitalOcean'
|
||||
: str($selectedType)->headline();
|
||||
@endphp
|
||||
@can('create', App\Models\CloudProviderToken::class)
|
||||
<x-modal-input title="New {{ $tokenProviderName }} token">
|
||||
<x-slot:content>
|
||||
<button type="button"
|
||||
class="button button-highlighted">
|
||||
<x-reicon name="plus" class="size-3.5" />
|
||||
New token
|
||||
</button>
|
||||
</x-slot:content>
|
||||
<livewire:security.cloud-provider-token-form :modal_mode="true" :provider="$tokenProvider"
|
||||
wire:key="new-server-token-{{ $tokenProvider }}" />
|
||||
</x-modal-input>
|
||||
@endcan
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
'uuid' => $server->uuid,
|
||||
'name' => $server->name,
|
||||
'description' => $server->description ?: 'No description',
|
||||
'address' => $server->ip,
|
||||
'href' => route('server.show', ['server_uuid' => $server->uuid]),
|
||||
'status' => $isReady ? 'Ready' : ($server->settings->force_disabled ? 'Disabled' : 'Validation required'),
|
||||
'statusType' => $isReady ? 'success' : 'error',
|
||||
@@ -41,7 +40,7 @@
|
||||
const query = this.search.trim().toLowerCase();
|
||||
if (!query) return this.servers;
|
||||
return this.servers.filter(server =>
|
||||
[server.name, server.description, server.address, server.status]
|
||||
[server.name, server.description, server.status]
|
||||
.some(value => String(value || '').toLowerCase().includes(query))
|
||||
);
|
||||
},
|
||||
@@ -109,7 +108,7 @@
|
||||
<h2 class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg"
|
||||
x-text="server.name"></h2>
|
||||
<p class="mt-0.5 truncate text-[11px] text-neutral-500 dark:text-fg-faint"
|
||||
x-text="server.address"></p>
|
||||
x-text="server.description"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto flex items-center pt-4">
|
||||
@@ -126,14 +125,13 @@
|
||||
<div x-show="viewMode === 'table'"
|
||||
class="overflow-x-auto rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
|
||||
<div
|
||||
class="grid min-w-[680px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_9.5rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
class="grid min-w-[480px] grid-cols-[minmax(0,1fr)_9.5rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
|
||||
<div>Server</div>
|
||||
<div>Address</div>
|
||||
<div>Status</div>
|
||||
</div>
|
||||
<template x-for="server in filteredServers" :key="server.uuid">
|
||||
<a :href="server.href" {{ wireNavigate() }}
|
||||
class="grid min-h-14 min-w-[680px] grid-cols-[minmax(0,1fr)_minmax(10rem,.7fr)_9.5rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
||||
class="grid min-h-14 min-w-[480px] grid-cols-[minmax(0,1fr)_9.5rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.035] dark:text-fg-dim">
|
||||
@@ -146,7 +144,6 @@
|
||||
x-text="server.description"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="truncate text-neutral-500 dark:text-fg-dim" x-text="server.address"></div>
|
||||
<div>
|
||||
<x-status-badge dynamic>
|
||||
<span class="size-1.5 rounded-full"
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
@if ($limit_reached)
|
||||
<x-limit-reached name="servers" />
|
||||
@elseif ($current_step === 1)
|
||||
<x-server.provider-token-picker provider="digitalocean" providerLabel="DigitalOcean"
|
||||
routeType="digital-ocean" :tokens="$available_tokens" />
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-server.provider-token-picker provider="digitalocean" providerLabel="DigitalOcean"
|
||||
routeType="digital-ocean" :tokens="$available_tokens" />
|
||||
<p class="text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
New to DigitalOcean?
|
||||
<a href="https://coolify.io/digitalocean" target="_blank"
|
||||
class="font-medium text-coollabs hover:underline dark:text-warning">Create an account</a>
|
||||
through Coolify's referral link.
|
||||
</p>
|
||||
</div>
|
||||
@elseif ($current_step === 2)
|
||||
<div wire:init="loadDigitalOceanData">
|
||||
@if ($loading_data)
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<div x-data="{ advancedOpen: false }"
|
||||
class="mt-5 border-t border-neutral-200 pt-4 dark:border-white/[0.08]">
|
||||
<button type="button" @click="advancedOpen = !advancedOpen"
|
||||
class="flex w-full items-center justify-between gap-3 text-left text-[13px] font-medium text-neutral-600 dark:text-fg-dim"
|
||||
class="flex w-full cursor-pointer items-center justify-between gap-3 text-left text-[13px] font-medium text-neutral-600 dark:text-fg-dim"
|
||||
:aria-expanded="advancedOpen">
|
||||
Advanced settings
|
||||
<x-reicon name="chevron-down" class="size-3.5 transition-transform"
|
||||
|
||||
@@ -2,8 +2,16 @@
|
||||
@if ($limit_reached)
|
||||
<x-limit-reached name="servers" />
|
||||
@elseif ($current_step === 1)
|
||||
<x-server.provider-token-picker provider="vultr" providerLabel="Vultr"
|
||||
:tokens="$available_tokens" />
|
||||
<div class="flex flex-col gap-6">
|
||||
<x-server.provider-token-picker provider="vultr" providerLabel="Vultr"
|
||||
:tokens="$available_tokens" />
|
||||
<p class="text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
New to Vultr?
|
||||
<a href="https://coolify.io/vultr" target="_blank"
|
||||
class="font-medium text-coollabs hover:underline dark:text-warning">Create an account</a>
|
||||
through Coolify's affiliate link.
|
||||
</p>
|
||||
</div>
|
||||
@elseif ($current_step === 2)
|
||||
<div wire:init="loadVultrData">
|
||||
@if ($loading_data)
|
||||
|
||||
@@ -14,3 +14,61 @@ test('server validation opens in the centered process dialog', function () {
|
||||
->toContain('@click="processDialogOpen = true"')
|
||||
->not->toContain('<x-slide-over closeWithX fullScreen>');
|
||||
});
|
||||
|
||||
test('DigitalOcean is available as an onboarding server provider', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/boarding/index.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('<x-modal-input title="Connect a DigitalOcean Server" isFullWidth>')
|
||||
->toContain('<x-digital-ocean-icon class="size-10 shrink-0" />')
|
||||
->toContain('Deploy servers directly from your DigitalOcean account.')
|
||||
->toContain('<livewire:server.new.by-digital-ocean :limit_reached="false" :from_onboarding="true" />');
|
||||
});
|
||||
|
||||
test('server type details are shown on the relevant cards instead of a technical details panel', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/boarding/index.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('aria-label="About this machine"')
|
||||
->toContain('aria-label="About remote servers"')
|
||||
->toContain('Not recommended for production workloads due to resource contention.')
|
||||
->toContain('Any SSH-accessible server, including cloud VPS, bare metal, and self-hosted infrastructure.')
|
||||
->not->toContain('<x-highlighted text="Servers" />')
|
||||
->not->toContain('<x-highlighted text="Localhost:" />')
|
||||
->not->toContain('<x-highlighted text="Remote Server:" />');
|
||||
});
|
||||
|
||||
test('server type cards use the standard card hover treatment', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/boarding/index.blade.php'));
|
||||
$hoverClasses = 'shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:shadow-md';
|
||||
preg_match_all('/min-h-36[^\"]*'.preg_quote($hoverClasses, '/').'/', $view, $matches);
|
||||
|
||||
expect($matches[0])->toHaveCount(5)
|
||||
->and(substr_count($view, 'group relative cursor-pointer'))->toBe(5)
|
||||
->and($view)->not->toContain('hover:border-coollabs/35 hover:bg-coollabs/[0.03]')
|
||||
->and($view)->not->toContain('dark:hover:border-warning/25 dark:hover:bg-warning/[0.04]');
|
||||
});
|
||||
|
||||
test('existing SSH key selection does not show a redundant value tooltip', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/boarding/index.blade.php'));
|
||||
|
||||
expect($view)->toContain('label="Existing SSH key" :options="$privateKeyOptions" :tooltip="false"');
|
||||
});
|
||||
|
||||
test('server connection step does not render a technical details panel', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/boarding/index.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->not->toContain('<x-highlighted text="Connection Requirements:" />')
|
||||
->not->toContain('<x-highlighted text="Hostname Resolution:" />')
|
||||
->not->toContain('<x-highlighted text="User Permissions:" />');
|
||||
});
|
||||
|
||||
test('project step does not render a technical details panel', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/boarding/index.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->not->toContain('<x-highlighted text="Project Organization:" />')
|
||||
->not->toContain('<x-highlighted text="Environments:" />')
|
||||
->not->toContain('<x-highlighted text="Team Access:" />');
|
||||
});
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerSetting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Once;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
DB::table('instance_settings')->insert(['id' => 0]);
|
||||
$user = User::factory()->create();
|
||||
$this->team = $user->teams()->first();
|
||||
|
||||
@@ -146,6 +150,38 @@ describe('ServerSetting::ensureValidSentinelToken', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ServerSetting::ensureSentinelUrl', function () {
|
||||
it('uses the current private instance URL when no public address is configured', function () {
|
||||
InstanceSettings::query()->whereKey(0)->update([
|
||||
'fqdn' => null,
|
||||
'public_ipv4' => null,
|
||||
'public_ipv6' => null,
|
||||
]);
|
||||
Once::flush();
|
||||
DB::table('server_settings')->where('id', $this->server->settings->id)->update(['sentinel_custom_url' => null]);
|
||||
app()->instance('request', Request::create('http://192.168.1.50:8000/server'));
|
||||
|
||||
$url = $this->server->settings->fresh()->ensureSentinelUrl();
|
||||
|
||||
expect($url)->toBe('http://192.168.1.50:8000')
|
||||
->and($this->server->settings->fresh()->sentinel_custom_url)->toBe($url);
|
||||
});
|
||||
|
||||
it('does not use a loopback request URL for a remote server', function () {
|
||||
InstanceSettings::query()->whereKey(0)->update([
|
||||
'fqdn' => null,
|
||||
'public_ipv4' => null,
|
||||
'public_ipv6' => null,
|
||||
]);
|
||||
Once::flush();
|
||||
DB::table('server_settings')->where('id', $this->server->settings->id)->update(['sentinel_custom_url' => null]);
|
||||
app()->instance('request', Request::create('http://localhost:8000/server'));
|
||||
|
||||
expect(fn () => $this->server->settings->fresh()->ensureSentinelUrl())
|
||||
->toThrow(RuntimeException::class, 'Set an instance FQDN, public IP, or reachable Coolify URL before enabling Sentinel.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generated sentinel tokens are valid', function () {
|
||||
it('generates tokens that pass format validation', function () {
|
||||
$settings = $this->server->settings;
|
||||
|
||||
@@ -5,6 +5,16 @@ test('server creation uses the standard page title without redundant navigation'
|
||||
|
||||
expect($view)
|
||||
->toContain('<h1 class="min-w-0 text-[24px]! leading-7! font-semibold! tracking-tight!">New server</h1>')
|
||||
->toContain('class="mb-5 flex min-h-9 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"')
|
||||
->not->toContain('Back to servers')
|
||||
->not->toContain('title="Add a server"');
|
||||
});
|
||||
|
||||
test('provider pages only show the token action in the account panel', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/server/create.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->not->toContain('New token')
|
||||
->not->toContain('new-server-token-')
|
||||
->not->toContain('tokenProviderName');
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ test('server creation keeps private key actions together and advanced options co
|
||||
->toContain('x-data="{ advancedOpen: false }"')
|
||||
->toContain('x-show="advancedOpen" x-cloak')
|
||||
->toContain('Advanced settings')
|
||||
->toContain('class="flex w-full cursor-pointer items-center justify-between gap-3')
|
||||
->toContain('label="Use as a dedicated build server"')
|
||||
->toContain('helper="Build servers compile applications but do not host deployments. Enabling this makes the server build-only."');
|
||||
});
|
||||
|
||||
@@ -20,3 +20,13 @@ test('view switchers use the shared coollabs selected state on every page', func
|
||||
->toContain('@utility control-selected')
|
||||
->toContain('@apply bg-coollabs text-white dark:bg-coollabs dark:text-white;');
|
||||
});
|
||||
|
||||
test('server index does not expose server IP addresses', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/server/index.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->not->toContain("'address' => \$server->ip")
|
||||
->not->toContain('<div>Address</div>')
|
||||
->not->toContain('x-text="server.address"')
|
||||
->not->toContain('server.address,');
|
||||
});
|
||||
|
||||
@@ -10,7 +10,8 @@ test('sentinel startup regenerates an empty endpoint from instance settings', fu
|
||||
->toContain('$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;')
|
||||
->and(file_get_contents(dirname(__DIR__, 2).'/app/Models/ServerSetting.php'))
|
||||
->toContain('generateSentinelUrl(ignoreEvent: true)')
|
||||
->toContain('Set an instance FQDN or public IP before enabling Sentinel.')
|
||||
->toContain('sentinelUrlFromCurrentRequest()')
|
||||
->toContain('Set an instance FQDN, public IP, or reachable Coolify URL before enabling Sentinel.')
|
||||
->and($component)
|
||||
->toContain('$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user