mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
revert: undo the app changes from the test clean-up
Revert the app code changes ofd666a0a95,69eeb2f62,1e66c9f9banda32082b03, so login, registration, OAuth, team danger zone, service templates, and the other touched files are back to their earlier state. Keep the removal of unused components and markup-only tests (d666a0a95also contained those deletions by mistake). StartupExecutionCleanupTest now fakes the template CDN with the current file content, so app:init in the test no longer empties templates/service-templates-latest.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a79ee909c6
commit
a6a3b1d26b
@@ -81,7 +81,7 @@ class CreateNewUser implements CreatesNewUsers
|
||||
}
|
||||
}
|
||||
// Set session variable
|
||||
session(['currentTeam' => $team]);
|
||||
session(['currentTeam' => $user->currentTeam = $team]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ class ServersController extends Controller
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$server = ModelsServer::whereTeamId($teamId)->whereUuid($request->route('uuid'))->first();
|
||||
$server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first();
|
||||
if (is_null($server)) {
|
||||
return response()->json(['message' => 'Server not found.'], 404);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Gitlab extends Controller
|
||||
{
|
||||
@@ -184,7 +185,7 @@ class Gitlab extends Controller
|
||||
continue;
|
||||
}
|
||||
|
||||
$deployment_uuid = new_public_id();
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
@@ -202,7 +203,7 @@ class Gitlab extends Controller
|
||||
'mode' => 'app',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $deployment_uuid,
|
||||
'deployment_uuid' => $deployment_uuid->toString(),
|
||||
'commit' => $commit,
|
||||
]);
|
||||
|
||||
@@ -274,7 +275,7 @@ class Gitlab extends Controller
|
||||
continue;
|
||||
}
|
||||
|
||||
$deployment_uuid = new_public_id();
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->first();
|
||||
|
||||
@@ -28,7 +28,8 @@ class TrustHosts extends Middleware
|
||||
}
|
||||
|
||||
// Skip host validation if no FQDN is configured (initial setup)
|
||||
if ($this->configuredFqdnHost() === null) {
|
||||
$fqdnHost = Cache::get('instance_settings_fqdn_host');
|
||||
if ($fqdnHost === '' || $fqdnHost === null) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
@@ -45,7 +46,27 @@ class TrustHosts extends Middleware
|
||||
{
|
||||
$trustedHosts = [];
|
||||
|
||||
$fqdnHost = $this->configuredFqdnHost();
|
||||
// Trust the configured FQDN from InstanceSettings (cached to avoid DB query on every request)
|
||||
// Use empty string as sentinel value instead of null so negative results are cached
|
||||
$fqdnHost = Cache::remember('instance_settings_fqdn_host', 300, function () {
|
||||
try {
|
||||
$settings = InstanceSettings::get();
|
||||
if ($settings && $settings->fqdn) {
|
||||
$url = Url::fromString($settings->fqdn);
|
||||
$host = $url->getHost();
|
||||
|
||||
return $host ?: '';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If instance settings table doesn't exist yet (during installation),
|
||||
// return empty string (sentinel) so this result is cached
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
// Convert sentinel value back to null for consumption
|
||||
$fqdnHost = $fqdnHost !== '' ? $fqdnHost : null;
|
||||
|
||||
if ($fqdnHost) {
|
||||
$trustedHosts[] = $fqdnHost;
|
||||
@@ -76,31 +97,4 @@ class TrustHosts extends Middleware
|
||||
|
||||
return array_filter($trustedHosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the configured instance FQDN host, cached to avoid a DB query on every request.
|
||||
*
|
||||
* An empty string is cached as the "no FQDN" sentinel so negative results are cached too.
|
||||
*/
|
||||
private function configuredFqdnHost(): ?string
|
||||
{
|
||||
$fqdnHost = Cache::remember('instance_settings_fqdn_host', 300, function () {
|
||||
try {
|
||||
$settings = InstanceSettings::get();
|
||||
if ($settings && $settings->fqdn) {
|
||||
$url = Url::fromString($settings->fqdn);
|
||||
$host = $url->getHost();
|
||||
|
||||
return $host ?: '';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If instance settings table doesn't exist yet (during installation),
|
||||
// return empty string (sentinel) so this result is cached
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
return $fqdnHost !== '' ? $fqdnHost : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,7 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
|
||||
->get(config('constants.services.official'));
|
||||
if ($response->successful()) {
|
||||
// Shared cache so Cloud HTTP nodes see the same bundle Horizon pulled.
|
||||
if (! store_service_templates_bundle($response->body())) {
|
||||
Log::error('PullTemplatesFromCDN received an invalid bundle', [
|
||||
'body' => str($response->body())->limit(500)->toString(),
|
||||
]);
|
||||
}
|
||||
store_service_templates_bundle($response->body());
|
||||
} else {
|
||||
Log::error('PullTemplatesFromCDN failed', [
|
||||
'status' => $response->status(),
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
|
||||
namespace App\Livewire\Profile;
|
||||
|
||||
use App\Services\AvatarStorageService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public int $userId;
|
||||
|
||||
public string $email;
|
||||
|
||||
@@ -17,7 +17,6 @@ use App\Traits\HasSecretManagerAutocomplete;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -189,6 +188,7 @@ class Show extends Component
|
||||
}
|
||||
|
||||
private function syncData(bool $toModel = false): void
|
||||
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->key = ValidationPatterns::normalizeEnvironmentVariableKey($this->key);
|
||||
@@ -330,8 +330,6 @@ class Show extends Component
|
||||
if ($this->is_required && $this->resource instanceof Service) {
|
||||
event(new ApplicationConfigurationChanged($this->resource->team()->id));
|
||||
}
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
|
||||
@@ -15,13 +15,7 @@ class ApiTokens extends Component
|
||||
|
||||
public ?string $description = null;
|
||||
|
||||
/**
|
||||
* Untyped on purpose: Livewire skips hydrating null into typed properties, which
|
||||
* would silently revert a "Never" (null) choice back to the default on the next request.
|
||||
*
|
||||
* @var int|string|null
|
||||
*/
|
||||
public $expiresInDays = 30;
|
||||
public ?int $expiresInDays = 30;
|
||||
|
||||
public $tokens = [];
|
||||
|
||||
@@ -149,7 +143,7 @@ class ApiTokens extends Component
|
||||
throw new \Exception('You do not have permission to create tokens with read:sensitive permissions.');
|
||||
}
|
||||
|
||||
$expiresAt = filled($validated['expiresInDays'] ?? null) ? now()->addDays((int) $validated['expiresInDays']) : null;
|
||||
$expiresAt = $this->expiresInDays ? now()->addDays($this->expiresInDays) : null;
|
||||
$token = auth()->user()->createToken($this->description, $permissions, $expiresAt);
|
||||
auditLog('ui.api_token.created', [
|
||||
'team_id' => currentTeam()->id,
|
||||
|
||||
@@ -296,9 +296,7 @@ class ByDigitalOcean extends Component
|
||||
$details = data_get($e->response->json(), $jsonMessageKey) ?: $e->response->body() ?: $details;
|
||||
}
|
||||
|
||||
$prefix = "{$providerName} API error: ";
|
||||
|
||||
return str_starts_with($details, $prefix) ? $details : $prefix.$details;
|
||||
return "{$providerName} API error: {$details}";
|
||||
}
|
||||
|
||||
public function getAvailableSizesProperty(): array
|
||||
|
||||
@@ -375,9 +375,7 @@ class ByVultr extends Component
|
||||
$details = data_get($e->response->json(), $jsonMessageKey) ?: $e->response->body() ?: $details;
|
||||
}
|
||||
|
||||
$prefix = "{$providerName} API error: ";
|
||||
|
||||
return str_starts_with($details, $prefix) ? $details : $prefix.$details;
|
||||
return "{$providerName} API error: {$details}";
|
||||
}
|
||||
|
||||
private function createVultrServer(VultrService $vultrService): array
|
||||
|
||||
@@ -163,7 +163,6 @@ class SettingsEmail extends Component
|
||||
public function toggleSmtp()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->resetErrorBag();
|
||||
|
||||
if ($this->smtpEnabled) {
|
||||
@@ -186,7 +185,6 @@ class SettingsEmail extends Component
|
||||
public function toggleResend()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->settings);
|
||||
$this->resetErrorBag();
|
||||
|
||||
if ($this->resendEnabled) {
|
||||
|
||||
@@ -16,7 +16,6 @@ class DangerZone extends Component
|
||||
public function mount(): void
|
||||
{
|
||||
$this->team = currentTeam();
|
||||
$this->authorize('delete', $this->team);
|
||||
}
|
||||
|
||||
public function delete(): mixed
|
||||
|
||||
@@ -27,7 +27,7 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
public function toResponse($request)
|
||||
{
|
||||
// First user (root) will be redirected to /settings instead of / on registration.
|
||||
if ($request->user()->currentTeam()?->id === 0) {
|
||||
if ($request->user()->currentTeam->id === 0) {
|
||||
return redirect()->route('settings.index');
|
||||
}
|
||||
|
||||
@@ -88,9 +88,9 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
if (! $user->teams()->where('team_id', $invitation->team->id)->exists()) {
|
||||
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
|
||||
}
|
||||
$team = $invitation->team;
|
||||
$user->currentTeam = $invitation->team;
|
||||
$invitation->delete();
|
||||
session(['currentTeam' => $team]);
|
||||
session(['currentTeam' => $user->currentTeam]);
|
||||
} else {
|
||||
// Restore the last active team; only fall back when unambiguous.
|
||||
$team = $user->resolveStoredTeam();
|
||||
@@ -98,7 +98,7 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
$team = $user->recreate_personal_team();
|
||||
}
|
||||
if ($team) {
|
||||
session(['currentTeam' => $team]);
|
||||
session(['currentTeam' => $user->currentTeam = $team]);
|
||||
}
|
||||
// Otherwise (multiple teams, no stored choice) leave the session
|
||||
// team unset so the user is sent to the team-selection screen.
|
||||
|
||||
@@ -29,7 +29,7 @@ class OauthLoginService
|
||||
: $this->resolveOauthUser($oauthUser, $oauthSetting, $email);
|
||||
|
||||
$team = $user->currentTeam() ?? $user->teams()->first() ?? $user->recreate_personal_team();
|
||||
session(['currentTeam' => $team]);
|
||||
session(['currentTeam' => $user->currentTeam = $team]);
|
||||
|
||||
if ($this->requiresTwoFactorChallenge($user)) {
|
||||
Auth::logout();
|
||||
|
||||
@@ -39,9 +39,7 @@ class ConfigurationDiffer
|
||||
$previous = $previousItems[$key] ?? null;
|
||||
$current = $currentItems[$key] ?? null;
|
||||
|
||||
// Static settings missing from an older snapshot were newly tracked, not changed.
|
||||
// Without any previous snapshot (legacy fallback), every setting is reported.
|
||||
if ($previous === null && $previousItems !== [] && ! in_array(data_get($current, 'section'), self::DYNAMIC_SECTIONS, true)) {
|
||||
if ($previous === null && ! in_array(data_get($current, 'section'), self::DYNAMIC_SECTIONS, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@ trait HasSecretManager
|
||||
|
||||
public static function bootHasSecretManager(): void
|
||||
{
|
||||
// Return nothing: a non-null result halts the "deleting" event and skips later listeners.
|
||||
static::deleting(function ($resource): void {
|
||||
$resource->secretManagerLink()->delete();
|
||||
});
|
||||
static::deleting(fn ($resource) => $resource->secretManagerLink()->delete());
|
||||
}
|
||||
|
||||
public function secretManagerLink(): MorphOne
|
||||
|
||||
@@ -1484,12 +1484,6 @@ function service_templates_path(): string
|
||||
*/
|
||||
function store_service_templates_bundle(string $json, ?string $fetchedAt = null): bool
|
||||
{
|
||||
// A 200 response can still be an empty body or an error page; keep the current templates then.
|
||||
$templates = json_decode($json, true);
|
||||
if (! is_array($templates) || $templates === [] || array_is_list($templates)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fetchedAt ??= now()->toIso8601String();
|
||||
$path = service_templates_path();
|
||||
|
||||
@@ -1536,9 +1530,7 @@ function get_service_templates(bool $force = false): Collection
|
||||
if ($response->failed()) {
|
||||
return collect([]);
|
||||
}
|
||||
if (! store_service_templates_bundle($response->body())) {
|
||||
return get_service_templates();
|
||||
}
|
||||
store_service_templates_bundle($response->body());
|
||||
|
||||
return collect(json_decode($response->body()))->sortKeys();
|
||||
} catch (Throwable) {
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
'active' => request()->routeIs('team.admin-view'),
|
||||
'icon' => 'admin',
|
||||
] : null,
|
||||
auth()->user()->can('delete', currentTeam()) ? [
|
||||
[
|
||||
'label' => 'Danger Zone',
|
||||
'route' => 'team.danger-zone',
|
||||
'active' => request()->routeIs('team.danger-zone'),
|
||||
'icon' => 'shield-alert',
|
||||
'sectionStart' => true,
|
||||
] : null,
|
||||
],
|
||||
])->filter();
|
||||
@endphp
|
||||
|
||||
|
||||
@@ -85,9 +85,7 @@
|
||||
$activeServerMenuItem['route'],
|
||||
$serverRouteParameters,
|
||||
);
|
||||
$showSentinelStatus = $server->isFunctional()
|
||||
&& $server->isSentinelEnabled()
|
||||
&& auth()->user()?->can('viewSentinel', $server);
|
||||
$showSentinelStatus = $server->isFunctional() && $server->isSentinelEnabled();
|
||||
$proxyCanBeStopped = in_array($proxyStatus, ['running', 'starting', 'restarting'], true);
|
||||
@endphp
|
||||
|
||||
|
||||
@@ -13,13 +13,6 @@
|
||||
:type="$server->isFunctional() ? 'success' : 'warning'" />
|
||||
</x-slot:actions>
|
||||
|
||||
@if ($this->limaStartCommand)
|
||||
<x-callout type="info" title="Start this Lima VM locally" class="mb-4">
|
||||
<code
|
||||
class="mt-2 block overflow-x-auto rounded-lg bg-neutral-950 px-3 py-2 font-mono text-[11px] text-neutral-200">{{ $this->limaStartCommand }}</code>
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-neutral-100 text-neutral-600 dark:bg-white/[0.06] dark:text-fg-dim">
|
||||
|
||||
@@ -88,24 +88,6 @@
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@if ($server->cloudProviderToken)
|
||||
@if ($provider === 'Hetzner' && !$server->isFunctional() && $hetznerServerStatus === 'off')
|
||||
<x-forms.button type="button" wire:click.prevent="startHetznerServer" isHighlighted
|
||||
canGate="update" :canResource="$server">
|
||||
Power On
|
||||
</x-forms.button>
|
||||
@elseif ($provider === 'DigitalOcean' && $digitalOceanDropletStatus === 'off')
|
||||
<x-forms.button type="button" wire:click.prevent="startDigitalOceanDroplet"
|
||||
isHighlighted canGate="update" :canResource="$server">
|
||||
Power On
|
||||
</x-forms.button>
|
||||
@elseif ($provider === 'Vultr' && $vultrInstanceStatus === 'stopped')
|
||||
<x-forms.button type="button" wire:click.prevent="startVultrInstance" isHighlighted
|
||||
canGate="update" :canResource="$server">
|
||||
Power On
|
||||
</x-forms.button>
|
||||
@endif
|
||||
@endif
|
||||
@endif
|
||||
@if ($server->server_metadata)
|
||||
<x-forms.button type="button" class="size-8! px-0!"
|
||||
|
||||
@@ -104,12 +104,7 @@
|
||||
<x-application.settings-section title="General"
|
||||
description="Connection and authentication settings for this private GitHub source.">
|
||||
<x-slot:actions>
|
||||
@if ($isConnected)
|
||||
<x-forms.button type="button" canGate="view" :canResource="$github_app" wire:click.prevent="testConnection">
|
||||
Test connection
|
||||
</x-forms.button>
|
||||
@endif
|
||||
<x-forms.button type="button" canGate="update" :canResource="$github_app" wire:click.prevent="updateGithubAppName">
|
||||
<x-forms.button type="button" wire:click.prevent="updateGithubAppName">
|
||||
<x-reicon name="refresh" class="size-3.5" />
|
||||
Sync name
|
||||
</x-forms.button>
|
||||
@@ -196,7 +191,7 @@
|
||||
shortConfirmationLabel="GitHub App Name" :confirmWithPassword="false"
|
||||
step2ButtonText="Permanently Delete" />
|
||||
@else
|
||||
<x-forms.button isError disabled canGate="delete" :canResource="$github_app" tooltip="You do not have permission to delete this GitHub App.">
|
||||
<x-forms.button isError disabled tooltip="You do not have permission to delete this GitHub App.">
|
||||
Delete
|
||||
</x-forms.button>
|
||||
@endcan
|
||||
|
||||
@@ -171,34 +171,17 @@ test('team index mounts when is_mcp_server_enabled is null on the session team',
|
||||
|
||||
// --- Team Danger Zone Livewire: delete ---
|
||||
|
||||
test('member cannot open the team danger zone', function () {
|
||||
test('member cannot delete team via danger zone', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(DangerZone::class)->assertForbidden();
|
||||
Livewire::test(DangerZone::class)
|
||||
->call('delete')
|
||||
->assertDispatched('error');
|
||||
|
||||
expect(Team::find($this->team->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('owner can open the team danger zone', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(DangerZone::class)->assertSuccessful();
|
||||
});
|
||||
|
||||
test('team settings show the danger zone link only to the team owner', function () {
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->actingAs($this->owner);
|
||||
Livewire::test(TeamIndex::class)->assertSee('Danger Zone');
|
||||
|
||||
foreach ([$this->admin, $this->member] as $user) {
|
||||
$this->actingAs($user);
|
||||
Livewire::test(TeamIndex::class)->assertDontSee('Danger Zone');
|
||||
}
|
||||
});
|
||||
|
||||
test('admin cannot delete team via policy', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
@@ -30,7 +30,12 @@ beforeEach(function () {
|
||||
|
||||
// Fake notifications to ensure none are sent
|
||||
Notification::fake();
|
||||
Http::fake();
|
||||
// app:init pulls the service templates and writes the real repository file,
|
||||
// so answer with its current content instead of an empty body.
|
||||
Http::fake([
|
||||
config('constants.services.official') => Http::response(file_get_contents(service_templates_path())),
|
||||
'*' => Http::response(),
|
||||
]);
|
||||
Process::fake();
|
||||
Queue::fake();
|
||||
|
||||
|
||||
@@ -119,33 +119,6 @@ it('skips pulling templates in local development', function () {
|
||||
expect(Cache::get(service_templates_cache_key()))->toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the local templates when the CDN returns an unusable bundle', function (string $body) {
|
||||
Http::fake([
|
||||
config('constants.services.official') => Http::response($body, 200),
|
||||
]);
|
||||
|
||||
$path = service_templates_path();
|
||||
$original = File::get($path);
|
||||
|
||||
try {
|
||||
config(['app.env' => 'production']);
|
||||
|
||||
(new PullTemplatesFromCDN)->handle();
|
||||
|
||||
expect(md5(File::get($path)))->toBe(md5($original))
|
||||
->and(Cache::get(service_templates_cache_key()))->toBeNull()
|
||||
->and(get_service_templates())->not->toBeEmpty();
|
||||
} finally {
|
||||
File::put($path, $original);
|
||||
Cache::forget(service_templates_cache_key());
|
||||
}
|
||||
})->with([
|
||||
'empty body' => '',
|
||||
'html error page' => '<html>Bad gateway</html>',
|
||||
'empty object' => '{}',
|
||||
'json list' => '[1, 2]',
|
||||
]);
|
||||
|
||||
it('logs when the CDN responds with a non-success status', function () {
|
||||
Http::fake([
|
||||
'cdn.coollabs.io/*' => Http::response('nope', 503),
|
||||
|
||||
Reference in New Issue
Block a user